<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Leisurelybear&#39;s Blog</title>
  
  <subtitle>Life, Games And Technology</subtitle>
  <link href="https://blog.lebear.top/atom.xml" rel="self"/>
  
  <link href="https://blog.lebear.top/"/>
  <updated>2023-04-09T08:28:53.283Z</updated>
  <id>https://blog.lebear.top/</id>
  
  <author>
    <name>Leisurelybear</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>由遥控车引起的不可靠的消息服务（监听断连、顺序颠倒、消息丢失）的问题及解决</title>
    <link href="https://blog.lebear.top/2022/01/12/452/"/>
    <id>https://blog.lebear.top/2022/01/12/452/</id>
    <published>2022-01-12T05:16:35.000Z</published>
    <updated>2023-04-09T08:28:53.283Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>背景：最近的项目中，有一个端到端控制的场景（即A发送命令，通过RTC服务，使B接收到消息并执行命令），其中为了开发方便使用了RTC服务作为两端通信的“桥梁“。每一组端，都连接到同一个RTC Room，这样一来，多组端都会互不影响。</p></blockquote><p>上述业务可以简单抽象成下图，<strong>手柄</strong> 发送消息 “<strong>前进5米</strong>“ 到RTC服务器，然后 <strong>遥控车</strong> 接受到该rtc消息后，执行 命令（<strong>前进5米</strong>）。抽象模型和执行流程如下：</p><p><img src="https://s4.ax1x.com/2021/12/29/T6lqat.png"></p><p>业务抽象表示</p><p><img src="https://s4.ax1x.com/2021/12/29/T635Bd.png"></p><p>执行流程</p><hr><p>All right！这一切看起来都是那么美好~~~</p><p><strong>但是</strong>！<strong>But</strong>！</p><p>这个RTC服务是某不知名厂商提供的<strong>极其不稳定</strong>的服务，具备以下特点：</p><ol><li><strong>会掉线。</strong></li><li><strong>时序不能保证。</strong></li><li><strong>可能会丢失消息</strong></li></ol><p>可谓是条条致命啊！在刚开始的业务中，由于使用并不频繁，所以rtc服务也就相对较”稳定”，上述问题并没有浮现出来。但是在业务扩张后，真的是招招致命啊！</p><p>既然使用了rtc，那么就要对她负责！（F**K (╯‵□′)╯︵┻━┻），So， 我们来针对这几个问题分析一下，是否可解？（当然是可解，不然就不会有下文了）</p><p><strong>首先，为什么会出现上述问题呢？</strong></p><p>通过查阅资料，了解到：WebRTC使用了流控传输协议（SCTP），这个协议啊，其实是很棒的一个协议，同时兼备了TCP和UDP的功能，当然也比较复杂。具有多路复用等优点。通信是先建立四次握手建立SCTP连接，通过channel以流的形式发送消息（如下图），该通道有多路，数据互不干涉，但是当一条通道堵塞后，会导致该连接整体中断。</p><p>但是其服务提供者大多场景是在音视频通话，所以并没有启用可靠传输。没错，正是因此，导致时序错误、丢失消息。上面说到的通道阻塞又会导致SCTP连接中断则是掉线的罪魁祸首。</p><p><img src="https://s4.ax1x.com/2021/12/30/T2MD3T.png"></p><p>SCTP通道，其中msg可以以有序（绿色）发送，也可以以无序（黄色）发送</p><hr><h2><span id="一-掉线问题">一、掉线问题</span></h2><p>首先解决掉线问题，掉线是个相对不可预测的事件。所以我们可以考虑通过心跳探活和重连机制来处理。</p><p>举个例子：小明和小O在通过某信打语音电话，但是小明的网络并不好，所以小明会每隔两分钟向小O问：能听到我讲话嘛？小O回复：可以的。来保证网络正常，通话仍在继续。</p><p><img src="https://s4.ax1x.com/2022/01/09/7iHdAK.gif"></p><p>以下是实现心跳的简单实现代码，其他细节则可忽略（例如小车移动）。</p><p>首先，我们模拟出RTC的实现，主要有RtcEvent接口用来实现事件监听、RtcMessage类统一消息体、RtcClient类则是Rtc的主要操作。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">package rtc;</span><br><span class="line">// 实现消息监听</span><br><span class="line">public interface RtcEvent &#123;</span><br><span class="line">    void handleMessage(RtcMessage msg);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">package rtc;</span><br><span class="line">// 统一化消息体</span><br><span class="line">public class RtcMessage &#123;</span><br><span class="line">    public String key; // 消息的Key</span><br><span class="line">    public int value; // 消息的内容</span><br><span class="line">    public long time; // 消息创建的时间戳</span><br><span class="line"></span><br><span class="line">    public RtcMessage() &#123;</span><br><span class="line">        this.time = System.currentTimeMillis();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>RtcClient也是模拟出来的，这部分主要模拟接收消息、掉线的情况。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><span class="line">package rtc;</span><br><span class="line">// 模拟Rtc功能SDK的实现</span><br><span class="line">public class RtcClient &#123;</span><br><span class="line"></span><br><span class="line">    private boolean shutdown = true;</span><br><span class="line"></span><br><span class="line">    public RtcClient(String uuid) &#123;</span><br><span class="line">        shutdown = false;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    // 监听发送来的心跳和消息，用线程来模拟</span><br><span class="line">    public void onListening(RtcEvent e) &#123;</span><br><span class="line"></span><br><span class="line">        // 模拟定时接收心跳的线程</span><br><span class="line">        Thread t = new Thread(() -&gt; &#123;</span><br><span class="line">            for (int i = 0; !shutdown ; i++) &#123;</span><br><span class="line">                RtcMessage heartbeat = new RtcMessage();</span><br><span class="line">                heartbeat.key = null;</span><br><span class="line">                e.handleMessage(heartbeat);</span><br><span class="line">                System.out.println(&quot;心跳：&quot;+ i);</span><br><span class="line"></span><br><span class="line">                // 1、模拟中间接收到消息</span><br><span class="line">                if (i == 10) &#123;</span><br><span class="line">                    // 执行命令</span><br><span class="line">                    RtcMessage cmdMsg = new RtcMessage();</span><br><span class="line">                    cmdMsg.key = &quot;MOVE&quot;;</span><br><span class="line">                    cmdMsg.value = 1000;</span><br><span class="line">                    e.handleMessage(cmdMsg);</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                // 2、模拟掉线</span><br><span class="line">                if (i == 15)&#123;</span><br><span class="line">                    try &#123;</span><br><span class="line">                        Thread.sleep(2000);</span><br><span class="line">                        break;</span><br><span class="line">                    &#125; catch (InterruptedException ex) &#123;</span><br><span class="line">                        ex.printStackTrace();</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">                try &#123;</span><br><span class="line">                    Thread.sleep(1000);</span><br><span class="line">                &#125; catch (InterruptedException ex) &#123;</span><br><span class="line">                    ex.printStackTrace();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">        t.start();</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    // 模拟发送出去心跳</span><br><span class="line">    public void send(RtcMessage msg) &#123;</span><br><span class="line">        System.out.println(&quot;send heartbeat: &quot; + msg.time);</span><br><span class="line">        //        e.handleMessage();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    // 停止</span><br><span class="line">    public void close() &#123;</span><br><span class="line">        // 让线程停止</span><br><span class="line">        this.shutdown = true;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>然后我们可以去开发小车端的功能，利用心跳机制，避免断连。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br></pre></td><td class="code"><pre><span class="line">import rtc.RtcClient;</span><br><span class="line">import rtc.RtcMessage;</span><br><span class="line"></span><br><span class="line">public class Car &#123;</span><br><span class="line"></span><br><span class="line">    // SDK提供的 rtc 客户端</span><br><span class="line">    private static RtcClient rtcClient;</span><br><span class="line">    // 判断小车是否已经开启</span><br><span class="line">    public static volatile boolean isOn;</span><br><span class="line">    // 心跳线程</span><br><span class="line">    private static Thread heartbeatThread;</span><br><span class="line">    // 连接 的 rtc room 的 id</span><br><span class="line">    private String uuid;</span><br><span class="line">    // 上一次心跳时间</span><br><span class="line">    private long lastHeartbeatTime = -1;</span><br><span class="line">    // 死亡沟壑时间，如果超过2000毫秒未接受到心跳，则说明挂了，需要重启client</span><br><span class="line">    public static final int DEAD_GAP_TIME = 2000;</span><br><span class="line"></span><br><span class="line">    public Car(String uuid)&#123;</span><br><span class="line">        this.uuid = uuid;</span><br><span class="line">        rtcClient = new RtcClient(uuid);</span><br><span class="line"></span><br><span class="line">        // 定时发送心跳的线程</span><br><span class="line">        heartbeatThread = new Thread(() -&gt; &#123;</span><br><span class="line">            for(;;)&#123;</span><br><span class="line">                // 探活，是否可以按时接收到心跳，如果不可以，则说明client可能断开连接了，需要重新连接</span><br><span class="line">                checkAndReset();</span><br><span class="line">                System.out.println(&quot;last heartbeat: &quot; + this.lastHeartbeatTime);</span><br><span class="line">                rtcClient.send(new RtcMessage());</span><br><span class="line">                try &#123;</span><br><span class="line">                    Thread.sleep(1000);</span><br><span class="line">                &#125; catch (InterruptedException e) &#123;</span><br><span class="line">                    e.printStackTrace();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">        // 开启心跳探活线程</span><br><span class="line">        heartbeatThread.start();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 重置 rtc 连接</span><br><span class="line">     */</span><br><span class="line">    private synchronized void checkAndReset() &#123;</span><br><span class="line">        if (lastHeartbeatTime == -1)&#123;</span><br><span class="line">            // 刚初始化，跳过</span><br><span class="line">            return;</span><br><span class="line">        &#125;</span><br><span class="line">        // 如果超过2000毫秒未接受到心跳，则说明挂了，需要重启client</span><br><span class="line">        if (System.currentTimeMillis() - lastHeartbeatTime &gt; DEAD_GAP_TIME)&#123;</span><br><span class="line">            if(rtcClient != null)&#123;</span><br><span class="line">                rtcClient.close();</span><br><span class="line">            &#125;</span><br><span class="line">            // 重新建立连接</span><br><span class="line">            rtcClient = new RtcClient(this.uuid);</span><br><span class="line">            startUp();// 重新监听</span><br><span class="line">            System.out.println(&quot;reset: &quot;+ rtcClient);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 开启小车</span><br><span class="line">     */</span><br><span class="line">    public synchronized void startUp()&#123;</span><br><span class="line"></span><br><span class="line">        // 开始监听 uuid room 的 rtc的消息</span><br><span class="line">        rtcClient.onListening(receiveMsg -&gt; &#123;</span><br><span class="line"></span><br><span class="line">            // 判断如果是心跳，则更新上一次探活时间</span><br><span class="line">            if (receiveMsg.key == null)&#123;</span><br><span class="line">                lastHeartbeatTime = receiveMsg.time;</span><br><span class="line">            &#125;else &#123;</span><br><span class="line">                // 执行rtc消息传来的命令，让小车进行相应的动作</span><br><span class="line">                executeCommand(receiveMsg.key, receiveMsg.value);</span><br><span class="line">                System.out.println(&quot;[ Cmd: &quot; + receiveMsg.key + &quot;, val: &quot; + receiveMsg.value + &quot; ]&quot;);</span><br><span class="line"></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 执行命令</span><br><span class="line">     * @param cmd 对应的指令</span><br><span class="line">     * @param v 值</span><br><span class="line">     */</span><br><span class="line">    private void executeCommand(String cmd, int v)&#123;</span><br><span class="line">        switch (cmd) &#123;</span><br><span class="line">            case &quot;MOVE&quot;:</span><br><span class="line">                move(v);</span><br><span class="line">                break;</span><br><span class="line">            case &quot;SPIN&quot;:</span><br><span class="line">                spin(v);</span><br><span class="line">                break;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 移动距离</span><br><span class="line">     * @param distance 距离</span><br><span class="line">     */</span><br><span class="line">    private void move(int distance)&#123;</span><br><span class="line">        System.out.println(&quot;move &quot; + distance);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 旋转角度</span><br><span class="line">     * @param angle 角度</span><br><span class="line">     */</span><br><span class="line">    private void spin(int angle)&#123;</span><br><span class="line">        System.out.println(&quot;spin &quot; + angle);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过上面的代码，我们可以发现，对于连接断开的问题，我们通过心跳机制顺利解决~即在心跳间隔大于一定时间时候则判定断开连接，这个时候就要重新连接。至此，我们已经解决了第一个问题。</p><p>我们通过一个Demo程序，看一下心跳效果。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">public class DemoMain &#123;</span><br><span class="line"></span><br><span class="line">    public static void main(String[] args) throws InterruptedException &#123;</span><br><span class="line">        Car c = new Car(&quot;10086&quot;);</span><br><span class="line">        c.startUp();</span><br><span class="line">        Thread.sleep(10000);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><img src="https://s4.ax1x.com/2022/01/11/7ZQQUg.png"></p><hr><h2><span id="二-消息时序问题">二、消息时序问题</span></h2><p>我们上面的代码是在Client中开启线程，使用for循环，来模拟心跳（heartbeat）以及命令（Command）。在实际生产环境中，心跳可以使用for循环来实现；但是命令是随时可能发送的，然而我们使用的SCTP协议的时候没有保证消息接受的顺序，所以会产生什么问题呢？看下面的gif，蓝色表示木板，黄色箭头表示小车和车头方向。</p><p>正常情况下，控制器发送三条命令，依次是 移动1、旋转90°、移动1。小车按顺序接收命令，则可以顺利到达指定位置（图2-1）。</p><p><img src="https://s4.ax1x.com/2022/01/09/7kmTeA.gif"></p><p>图2-1 正常情况</p><p>但是，如果消息并不是按顺序到达，而是乱序，就会出现下面的情况（图2-2），先接收到 SPIN，然后接收到 MOVE，就会导致小车从木板上掉落下去。</p><p><img src="https://s4.ax1x.com/2022/01/09/7knwkt.gif"></p><p>图2-2 乱序情况</p><p>显然，我们应该避免（图2-2）这样的情况，所以如何解决呢？我们可以借鉴TCP的机制。</p><p>既然大家看到了这篇文章，想必大家都或多或少了解TCP协议吧，在TCP协议中，通过SEQ和ACK的机制来确保报文顺序。具体是指发送方将要发送的数据分割成合适大小的报文段，然后在每个报文段标上序号，这个序号就是SEQ，接收端在接收到报文后，发送ACK来反馈自己已经接受到的数据报文，以及通知发送方下次发送的报文序列号。这个期间，接收方可能以乱序的形式接受到这些报文，那么在接收后，对报文进行排序，即可达到数据顺序正确的目的。</p><p><img src="https://s4.ax1x.com/2022/01/09/7kMESf.png"></p><p>图源自网络（<a href="https://www.cnblogs.com/silyvin/p/11927398.html%EF%BC%89">https://www.cnblogs.com/silyvin/p/11927398.html）</a></p><p>分析到这里，我们可以试一试走Seq这条路，给每个消息打上Seq，然后通过Seq来保证时序，这里先以简单的实现方式来出发，不考虑消息丢失，只考虑消息乱序。</p><p>首先，改造RtcMessage，使其支持消息序号</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">package rtc;</span><br><span class="line"></span><br><span class="line">public class RtcMessage implements Comparable&lt;RtcMessage&gt; &#123;</span><br><span class="line">    public String key;</span><br><span class="line">    public int value;</span><br><span class="line">    public long time;</span><br><span class="line">    public long seq;</span><br><span class="line"></span><br><span class="line">    public RtcMessage() &#123;</span><br><span class="line">        this.time = System.currentTimeMillis();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    @Override // 实现比较，为了让消息自排序，后面会讲为什么要实现这个方法</span><br><span class="line">    public int compareTo(RtcMessage o) &#123;</span><br><span class="line">        if (this.seq &gt; o.seq) &#123;</span><br><span class="line">            return -1;</span><br><span class="line">        &#125;</span><br><span class="line">        return 1;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后改造RtcClient，模拟发送多条指令，以多线程的方式发送，并携带序号。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br></pre></td><td class="code"><pre><span class="line">package rtc;</span><br><span class="line"></span><br><span class="line">import java.util.ArrayList;</span><br><span class="line">import java.util.List;</span><br><span class="line"></span><br><span class="line">public class RtcClient &#123;</span><br><span class="line"></span><br><span class="line">    // 使用标志位来控制线程的停止</span><br><span class="line">    private boolean shutdown = true;</span><br><span class="line"></span><br><span class="line">    public RtcClient(String uuid) &#123;</span><br><span class="line">        shutdown = false;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    // 监听发送来的心跳和消息，用线程来模拟</span><br><span class="line">    public void onListening(RtcEvent e) &#123;</span><br><span class="line"></span><br><span class="line">        // 定时发送心跳的线程</span><br><span class="line">        Thread t = new Thread(() -&gt; &#123;</span><br><span class="line">            for (int i = 0; !shutdown ; i++) &#123;</span><br><span class="line">                RtcMessage heartbeat = new RtcMessage();</span><br><span class="line">                heartbeat.key = null;</span><br><span class="line">                e.handleMessage(heartbeat);</span><br><span class="line">                System.out.println(&quot;心跳：&quot;+ i);</span><br><span class="line"></span><br><span class="line">                // 模拟中间接收到消息</span><br><span class="line">                if (i == 10) &#123;</span><br><span class="line"></span><br><span class="line">                    // 正确顺序，0-MOVE、1-SPIN、2-MOVE、3-SPIN、4-MOVE</span><br><span class="line">                    // 先生成顺序消息</span><br><span class="line">                    List&lt;RtcMessage&gt; msgList = new ArrayList&lt;&gt;(); // 使用list装</span><br><span class="line">                    for(int j = 0; j &lt; 5; j++)&#123;</span><br><span class="line">                        RtcMessage cmdMsg = new RtcMessage();</span><br><span class="line">                        if ((j &amp; 1) == 0)&#123;</span><br><span class="line">                            // 执行命令</span><br><span class="line">                            cmdMsg.key = &quot;MOVE&quot;;</span><br><span class="line">                            cmdMsg.value = 1000;</span><br><span class="line">                        &#125;else &#123;</span><br><span class="line">                            // 执行命令</span><br><span class="line">                            cmdMsg.key = &quot;SPIN&quot;;</span><br><span class="line">                            cmdMsg.value = 90;</span><br><span class="line">                        &#125;</span><br><span class="line">                        cmdMsg.seq = j;</span><br><span class="line">                        msgList.add(cmdMsg);</span><br><span class="line">                    &#125;</span><br><span class="line">                    // 然后开启多线程发送指令</span><br><span class="line">                    msgList.forEach(msg -&gt; &#123;</span><br><span class="line">                        new Thread(() -&gt; &#123;</span><br><span class="line">                            e.handleMessage(msg);</span><br><span class="line">                        &#125;).start();</span><br><span class="line">                    &#125;);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                // 模拟掉线</span><br><span class="line">                if (i == 15)&#123;</span><br><span class="line">                    try &#123;</span><br><span class="line">                        Thread.sleep(2000);</span><br><span class="line">                        break;</span><br><span class="line">                    &#125; catch (InterruptedException ex) &#123;</span><br><span class="line">                        ex.printStackTrace();</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">                try &#123;</span><br><span class="line">                    Thread.sleep(1000);</span><br><span class="line">                &#125; catch (InterruptedException ex) &#123;</span><br><span class="line">                    ex.printStackTrace();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">        t.start();</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    // 模拟发送出去心跳</span><br><span class="line">    public void send(RtcMessage msg) &#123;</span><br><span class="line">        System.out.println(&quot;send heartbeat: &quot; + msg.time);</span><br><span class="line">        //        e.handleMessage();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    // 停止</span><br><span class="line">    public void close() &#123;</span><br><span class="line">        // 让线程停止</span><br><span class="line">        this.shutdown = true;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>然后，我们在Car中打印出执行的指令和序列号，出现了下面的情况，小车接收到的指令顺序并不正确，但是如果我们直接按照这个顺序执行（转180°，再走3000米），就会出大问题；而我们期望的是（走1000米，转90°，走1000米，转90°，走1000米）。</p><p><img src="https://s4.ax1x.com/2022/01/11/7ZQs2R.png"></p><p>我们上文中引入了Seq的概念，所以接下来，我们在小车接收端（Car）也使用以下吧~</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br></pre></td><td class="code"><pre><span class="line">import rtc.RtcClient;</span><br><span class="line">import rtc.RtcMessage;</span><br><span class="line"></span><br><span class="line">import java.util.concurrent.PriorityBlockingQueue;</span><br><span class="line">import java.util.concurrent.atomic.AtomicLong;</span><br><span class="line">import java.util.concurrent.locks.ReentrantLock;</span><br><span class="line"></span><br><span class="line">public class Car &#123;</span><br><span class="line"></span><br><span class="line">    // SDK提供的 rtc 客户端</span><br><span class="line">    private static RtcClient rtcClient;</span><br><span class="line">    // 判断小车是否已经开启</span><br><span class="line">    public static volatile boolean isOn;</span><br><span class="line">    // 心跳线程</span><br><span class="line">    private static Thread heartbeatThread;</span><br><span class="line">    // 连接 的 rtc room 的 id</span><br><span class="line">    private String uuid;</span><br><span class="line">    // 上一次心跳时间</span><br><span class="line">    private long lastHeartbeatTime = -1;</span><br><span class="line">    // 死亡沟壑时间，如果超过2000毫秒未接受到心跳，则说明挂了，需要重启client</span><br><span class="line">    private static final int DEAD_GAP_TIME = 2000;</span><br><span class="line">    // 记录上一次消费的命令Seq</span><br><span class="line">    private static final AtomicLong lastSeq = new AtomicLong();</span><br><span class="line">    // 顺序队列</span><br><span class="line">    private static final PriorityBlockingQueue&lt;RtcMessage&gt; waitingQueue = new PriorityBlockingQueue&lt;&gt;();</span><br><span class="line">    // 锁</span><br><span class="line">    private static final ReentrantLock lock = new ReentrantLock();</span><br><span class="line"></span><br><span class="line">    public Car(String uuid) &#123;</span><br><span class="line">        this.uuid = uuid;</span><br><span class="line">        rtcClient = new RtcClient(uuid);</span><br><span class="line"></span><br><span class="line">        // 定时发送心跳的线程</span><br><span class="line">        heartbeatThread = new Thread(() -&gt; &#123;</span><br><span class="line">            for (; ; ) &#123;</span><br><span class="line">                // 探活，是否可以按时接收到心跳，如果不可以，则说明client可能断开连接了，需要重新连接</span><br><span class="line">                checkAndReset();</span><br><span class="line">                System.out.println(&quot;last heartbeat: &quot; + this.lastHeartbeatTime);</span><br><span class="line">                rtcClient.send(new RtcMessage());</span><br><span class="line">                try &#123;</span><br><span class="line">                    Thread.sleep(1000);</span><br><span class="line">                &#125; catch (InterruptedException e) &#123;</span><br><span class="line">                    e.printStackTrace();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">        // 开启心跳探活线程</span><br><span class="line">        heartbeatThread.start();</span><br><span class="line">        initMsgWaitingQueue();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 初始化命令消费等待队列，开启线程，轮询等待队列，做到有顺序消费（执行命令）</span><br><span class="line">     */</span><br><span class="line">    private void initMsgWaitingQueue() &#123;</span><br><span class="line">        // 初始化首次消费命令顺序为0，以后每消费一次，序列号都+1，这个在tcp中可以作为ACK</span><br><span class="line">        new Thread(() -&gt; &#123;</span><br><span class="line">            for (; ; ) &#123;</span><br><span class="line">                lock.lock();</span><br><span class="line">                System.out.println(&quot;wait for command...&quot;);</span><br><span class="line">                if (waitingQueue.size() == 0) &#123;</span><br><span class="line">                    // 还没有消息</span><br><span class="line">//                    System.out.println(&quot;null message.&quot;);</span><br><span class="line">                &#125; else &#123;</span><br><span class="line">                    RtcMessage receiveMsg = waitingQueue.peek();</span><br><span class="line"></span><br><span class="line">                    // 如果本次有消息</span><br><span class="line">                    // 如果这次的消息序列号不等于上次+1，即 seq != lastSeq + 1，则还需要再把它入队列，再等等</span><br><span class="line">                    if (receiveMsg.seq != lastSeq.get()) &#123;</span><br><span class="line">                        // 既然不相等，那就需要再减回去</span><br><span class="line">                        System.out.printf(&quot;receiveMsg seq: %d, last seq: %d\n&quot;, receiveMsg.seq, lastSeq.get());</span><br><span class="line">                        System.out.println(&quot;not sequence message, continue wait...&quot;);</span><br><span class="line">                    &#125; else &#123;</span><br><span class="line">                        // 若 seq = lastSeq + 1，则直接执行命令，进行消费</span><br><span class="line">                        executeCommand(receiveMsg.key, receiveMsg.value);</span><br><span class="line">                        lastSeq.addAndGet(1); // 标记上次消费的位置</span><br><span class="line">                        System.out.println(&quot;[ Seq: &quot; + receiveMsg.seq + &quot;, Cmd: &quot; + receiveMsg.key + &quot;, val: &quot; + receiveMsg.value + &quot; ]&quot;);</span><br><span class="line">                        waitingQueue.poll();</span><br><span class="line">                        continue;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">                lock.unlock();</span><br><span class="line"></span><br><span class="line">                // 等一下消息吧</span><br><span class="line">                try &#123;</span><br><span class="line">                    Thread.sleep(1000);</span><br><span class="line">                &#125; catch (InterruptedException e) &#123;</span><br><span class="line">                    e.printStackTrace();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">        &#125;).start();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 重置 rtc 连接</span><br><span class="line">     */</span><br><span class="line">    private synchronized void checkAndReset() &#123;</span><br><span class="line">        if (lastHeartbeatTime == -1)&#123;</span><br><span class="line">            // 刚初始化，跳过</span><br><span class="line">            return;</span><br><span class="line">        &#125;</span><br><span class="line">        // 如果超过2000毫秒未接受到心跳，则说明挂了，需要重启client</span><br><span class="line">        if (System.currentTimeMillis() - lastHeartbeatTime &gt; DEAD_GAP_TIME) &#123;</span><br><span class="line">            if (rtcClient != null) &#123;</span><br><span class="line">                rtcClient.close();</span><br><span class="line">            &#125;</span><br><span class="line">            // 重新建立连接</span><br><span class="line">            rtcClient = new RtcClient(this.uuid);</span><br><span class="line">            startUp();// 重新监听</span><br><span class="line">            System.out.println(&quot;reset: &quot; + rtcClient);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 开启小车</span><br><span class="line">     */</span><br><span class="line">    public synchronized void startUp() &#123;</span><br><span class="line"></span><br><span class="line">        // 开始监听 uuid room 的 rtc的消息</span><br><span class="line">        rtcClient.onListening(receiveMsg -&gt; &#123;</span><br><span class="line"></span><br><span class="line">            // 判断如果是心跳，则更新上一次探活时间</span><br><span class="line">            if (receiveMsg.key == null) &#123;</span><br><span class="line">                lastHeartbeatTime = receiveMsg.time;</span><br><span class="line">            &#125; else &#123;</span><br><span class="line">                // 执行rtc消息传来的命令，让小车进行相应的动作</span><br><span class="line">//                executeCommand(receiveMsg.key, receiveMsg.value);</span><br><span class="line">//                System.out.println(&quot;[ Seq: &quot; + receiveMsg.seq + &quot;, Cmd: &quot; + receiveMsg.key + &quot;, val: &quot; + receiveMsg.value + &quot; ]&quot;);</span><br><span class="line">                addWaitingQueue(receiveMsg); // 添加等待队列，来代替直接消费，这里使用优先队列保证顺序</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * @param receiveMsg 接收到的消息</span><br><span class="line">     */</span><br><span class="line">    private void addWaitingQueue(RtcMessage receiveMsg) &#123;</span><br><span class="line">        // 加个锁吧~</span><br><span class="line">        lock.lock();</span><br><span class="line">        waitingQueue.offer(receiveMsg);</span><br><span class="line">        lock.unlock();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 执行命令</span><br><span class="line">     *</span><br><span class="line">     * @param cmd 对应的指令</span><br><span class="line">     * @param v   值</span><br><span class="line">     */</span><br><span class="line">    private void executeCommand(String cmd, int v) &#123;</span><br><span class="line">        switch (cmd) &#123;</span><br><span class="line">            case &quot;MOVE&quot;:</span><br><span class="line">                move(v);</span><br><span class="line">                break;</span><br><span class="line">            case &quot;SPIN&quot;:</span><br><span class="line">                spin(v);</span><br><span class="line">                break;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 移动距离</span><br><span class="line">     *</span><br><span class="line">     * @param distance 距离</span><br><span class="line">     */</span><br><span class="line">    private void move(int distance) &#123;</span><br><span class="line">        System.out.println(&quot;move &quot; + distance);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 旋转角度</span><br><span class="line">     *</span><br><span class="line">     * @param angle 角度</span><br><span class="line">     */</span><br><span class="line">    private void spin(int angle) &#123;</span><br><span class="line">        System.out.println(&quot;spin &quot; + angle);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public void close() &#123;</span><br><span class="line">        synchronized (waitingQueue) &#123;</span><br><span class="line">            waitingQueue.forEach(c -&gt; System.out.print(&quot;*****************&quot; + c.seq + &quot;, &quot;));</span><br><span class="line">            System.out.println();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><img src="https://s4.ax1x.com/2022/01/12/7nSNo6.png"></p><p>嘻嘻，解决啦~</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">上面通过对 小车（Car） 的改造，将【收到命令立即执行命令】改为【收到命令后先放入优先队列】，然后【对命令进行有序执行】，顺利的解决了命令乱序的问题，真的是 Awesome ~</span><br></pre></td></tr></table></figure><h2><span id="三-消息丢失问题">三、消息丢失问题</span></h2><p>按顺序看下来的小伙伴们可以从上面了解到，在处理命令乱序问题时候，我们默认消息是不丢失的；但是呢，这个消息可并不是那么乐观，可能存在丢失的情况，这可就不好了，既然消息丢失，那我们的命令很可能无法接收到，也会出现很严重的后果！</p><p>当然，这个我们也是有解滴<del>有小伙伴可能会讲了：这不就用上面说到的TCP的可靠传输的方案来解决不就可以了嘛？没错，可以！但是笔者在这里小小偷了个懒，使用了一种不是很好但能快速解决问题的方案：多倍发包。至于TCP的可靠传输，可以下来自己尝试实现一下</del></p><p>我们看一下，什么是多倍发包。</p><p>举个栗子：快要过年了，小明想要抢票回家，但是呢，一票难求，大家都懂。所以小明在放票前，打开了10个相同的买票窗口，在到点时，快速的把每个窗口都点了一遍买票。由于小明的“多倍发包”，成功抢到了回家的票~</p><p>对于这个消息丢失，我们可以通过每次执行命令，发多个相同的指令，在小车接收端，对于相同序号的指令只执行一遍，这样就可以在即使部分指令丢了的情况下，也最大程度保证消息的完整。下面我们来实现一下吧~</p><p>首先，模拟消息丢失，在 Car 中的 startUp() 开始监听消息的方法中，加上随机丢失的代码，如下</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">// Car.class</span><br><span class="line">    /**</span><br><span class="line">     * 开启小车</span><br><span class="line">     */</span><br><span class="line">    public synchronized void startUp() &#123;</span><br><span class="line"></span><br><span class="line">        // 开始监听 uuid room 的 rtc的消息</span><br><span class="line">        rtcClient.onListening(receiveMsg -&gt; &#123;</span><br><span class="line">            // 判断如果是心跳，则更新上一次探活时间</span><br><span class="line">            if (receiveMsg.key == null) &#123;</span><br><span class="line">                lastHeartbeatTime = receiveMsg.time;</span><br><span class="line">            &#125; else &#123;</span><br><span class="line">                // 执行rtc消息传来的命令，让小车进行相应的动作</span><br><span class="line"></span><br><span class="line">                if(new Random().nextBoolean())&#123;</span><br><span class="line">                    // 模拟随机丢包</span><br><span class="line">                    System.out.println(&quot;消息丢失啦【&quot; + receiveMsg.seq + &quot;, &quot;+receiveMsg.key + &quot;: &quot; + receiveMsg.value + &quot;】&quot;);</span><br><span class="line">                    return;</span><br><span class="line">                &#125;</span><br><span class="line">//                executeCommand(receiveMsg.key, receiveMsg.value);</span><br><span class="line">//                System.out.println(&quot;[ Seq: &quot; + receiveMsg.seq + &quot;, Cmd: &quot; + receiveMsg.key + &quot;, val: &quot; + receiveMsg.value + &quot; ]&quot;);</span><br><span class="line">                addWaitingQueue(receiveMsg); // 添加等待队列，来代替直接消费，这里使用优先队列保证顺序</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>我们执行一下，下图就是模拟在接收数据时，1、4两个命令都丢失了，导致程序无法正常执行命令。</p><p><img src="https://s4.ax1x.com/2022/01/13/7M9Wng.png"></p><p>丢包情况</p><p>接下来，添加多倍发包，在RtcClient中模拟，在OnListening()方法中，同样的指令发送多次，如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line">// RtcClient.class</span><br><span class="line">    // 这里可以设置多倍发包的倍数</span><br><span class="line">    private static final int MULTIPLE_MESSAGE_SEND_TIMES = 3;</span><br><span class="line">// 监听发送来的心跳和消息，用线程来模拟</span><br><span class="line">    public void onListening(RtcEvent e) &#123;</span><br><span class="line"></span><br><span class="line">        // 定时发送心跳的线程</span><br><span class="line">        Thread t = new Thread(() -&gt; &#123;</span><br><span class="line">            for (int i = 0; !shutdown ; i++) &#123;</span><br><span class="line">                RtcMessage heartbeat = new RtcMessage();</span><br><span class="line">                heartbeat.key = null;</span><br><span class="line">                e.handleMessage(heartbeat);</span><br><span class="line">                System.out.println(&quot;心跳：&quot;+ i);</span><br><span class="line"></span><br><span class="line">                // 模拟中间接收到消息</span><br><span class="line">                if (i == 10) &#123;</span><br><span class="line"></span><br><span class="line">                    // 正确顺序，0-MOVE、1-SPIN、2-MOVE、3-SPIN、4-MOVE</span><br><span class="line">                    // 先生成顺序消息</span><br><span class="line">                    List&lt;RtcMessage&gt; msgList = new ArrayList&lt;&gt;(); // 使用list装</span><br><span class="line">                    for(int j = 0; j &lt; 5; j++)&#123;</span><br><span class="line">                        RtcMessage cmdMsg = new RtcMessage();</span><br><span class="line">                        if ((j &amp; 1) == 0)&#123;</span><br><span class="line">                            // 执行命令</span><br><span class="line">                            cmdMsg.key = &quot;MOVE&quot;;</span><br><span class="line">                            cmdMsg.value = 1000;</span><br><span class="line">                        &#125;else &#123;</span><br><span class="line">                            // 执行命令</span><br><span class="line">                            cmdMsg.key = &quot;SPIN&quot;;</span><br><span class="line">                            cmdMsg.value = 90;</span><br><span class="line">                        &#125;</span><br><span class="line">                        cmdMsg.seq = j;</span><br><span class="line">                        msgList.add(cmdMsg);</span><br><span class="line">                    &#125;</span><br><span class="line">                    // 然后开启多线程发送指令</span><br><span class="line">                    msgList.forEach(msg -&gt; &#123;</span><br><span class="line">                        new Thread(() -&gt; &#123;</span><br><span class="line">                            // 多倍发包</span><br><span class="line">                            for (int j = 0; j &lt; MULTIPLE_MESSAGE_SEND_TIMES; j++) &#123;</span><br><span class="line">                                e.handleMessage(msg);</span><br><span class="line">                            &#125;</span><br><span class="line">                        &#125;).start();</span><br><span class="line">                    &#125;);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                // 模拟掉线</span><br><span class="line">                if (i == 15)&#123;</span><br><span class="line">                    try &#123;</span><br><span class="line">                        Thread.sleep(2000);</span><br><span class="line">                        break;</span><br><span class="line">                    &#125; catch (InterruptedException ex) &#123;</span><br><span class="line">                        ex.printStackTrace();</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">                try &#123;</span><br><span class="line">                    Thread.sleep(1000);</span><br><span class="line">                &#125; catch (InterruptedException ex) &#123;</span><br><span class="line">                    ex.printStackTrace();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">        t.start();</span><br><span class="line"></span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>执行，虽然这次能保证客户端能接收到每个Seq至少一条消息，但是，有些Seq的消息接收了多遍，如果都执行，那么也不符合预期。如下图，丢失了 【Seq&#x3D;0、0、1、1、2、3、4、4】 这八条消息，接受到了【Seq&#x3D;0、1、2、2、3、3、4】这七条消息，执行结果会如下，我们只能正确执行【0、1、2】三条消息的指令，不能继续往下执行了，因为当前队列头部为2，但是我们的lastSeq已经指向3，所以我们不能继续执行2.</p><p><img src="https://s4.ax1x.com/2022/01/13/7MCrb4.png"></p><p>多倍发包</p><p>所以我们继续修改Car小车接收端，如果命令被执行过，则直接丢掉，不进入阻塞队列，这样我们就可以继续有序的执行命令。只需要对监听 waitingQueue 的方法进行处理即可，让重复的命令包丢掉。下面是对Car类下的 initMsgWaitingQueue() 方法进行改造，在判断队列内消息的开始，先判断是否已经接收过这个Seq，接受过的话直接丢掉。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br></pre></td><td class="code"><pre><span class="line">Car.class</span><br><span class="line">    /**</span><br><span class="line">     * 初始化命令消费等待队列，开启线程，轮询等待队列，做到有顺序消费（执行命令）</span><br><span class="line">     */</span><br><span class="line">    private void initMsgWaitingQueue() &#123;</span><br><span class="line">        // 初始化首次消费命令顺序为0，以后每消费一次，序列号都+1，这个在tcp中可以作为ACK</span><br><span class="line">        new Thread(() -&gt; &#123;</span><br><span class="line">            for (; ; ) &#123;</span><br><span class="line">                lock.lock();</span><br><span class="line">                System.out.println(&quot;wait for command...&quot;);</span><br><span class="line">                if (waitingQueue.size() == 0) &#123;</span><br><span class="line">                    // 还没有消息</span><br><span class="line">//                    System.out.println(&quot;null message.&quot;);</span><br><span class="line">                &#125; else &#123;</span><br><span class="line">                    RtcMessage receiveMsg = waitingQueue.peek();</span><br><span class="line">                    // 如果队首的Command的Seq小于当前执行的lastSeq，那么说明这条命令已经被执行了，所以丢掉</span><br><span class="line">                    if (receiveMsg.seq &lt; lastSeq.get())&#123;</span><br><span class="line">                        waitingQueue.poll(); // 丢掉</span><br><span class="line">                        continue; // 直接进行下一轮</span><br><span class="line">                    &#125;</span><br><span class="line"></span><br><span class="line">                    // 如果本次有消息</span><br><span class="line">                    // 如果这次的消息序列号不等于上次+1，即 seq != lastSeq + 1，则还需要再把它入队列，再等等</span><br><span class="line">                    if (receiveMsg.seq != lastSeq.get()) &#123;</span><br><span class="line">                        // 既然不相等，那就需要再减回去</span><br><span class="line">                        System.out.printf(&quot;receiveMsg seq: %d, last seq: %d\n&quot;, receiveMsg.seq, lastSeq.get());</span><br><span class="line">                        System.out.println(&quot;not sequence message, continue wait...&quot;);</span><br><span class="line">                    &#125; else &#123;</span><br><span class="line">                        // 若 seq = lastSeq + 1，则直接执行命令，进行消费</span><br><span class="line">                        executeCommand(receiveMsg.key, receiveMsg.value);</span><br><span class="line">                        lastSeq.addAndGet(1); // 标记上次消费的位置</span><br><span class="line">                        System.out.println(&quot;[ Seq: &quot; + receiveMsg.seq + &quot;, Cmd: &quot; + receiveMsg.key + &quot;, val: &quot; + receiveMsg.value + &quot; ]&quot;);</span><br><span class="line">                        waitingQueue.poll();</span><br><span class="line">                        continue;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">                lock.unlock();</span><br><span class="line"></span><br><span class="line">                // 等一下消息吧</span><br><span class="line">                try &#123;</span><br><span class="line">                    Thread.sleep(1000);</span><br><span class="line">                &#125; catch (InterruptedException e) &#123;</span><br><span class="line">                    e.printStackTrace();</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">//                    try &#123;</span><br><span class="line">//                        RtcMessage receiveMsg = waitingQueue.poll(1, TimeUnit.SECONDS);</span><br><span class="line">//                        if (receiveMsg == null) &#123;</span><br><span class="line">//                            System.out.println(&quot;null message.&quot;);</span><br><span class="line">//                            continue;</span><br><span class="line">//                        &#125;</span><br><span class="line">//                        waitingQueue.forEach(c -&gt; System.out.println(c.seq));</span><br><span class="line">//                        // 如果这次的消息序列号不等于上次+1，即 seq != lastSeq + 1，则还需要再把它入队列，再等等</span><br><span class="line">//                        if (receiveMsg.seq != lastSeq.get() + 1) &#123;</span><br><span class="line">//                            addWaitingQueue(receiveMsg); // 再次入队</span><br><span class="line">//                            Thread.sleep(1000); // 再等待一秒吧</span><br><span class="line">//                            continue;</span><br><span class="line">//                        &#125;</span><br><span class="line">//                        // 若 seq = lastSeq + 1，则直接执行命令</span><br><span class="line">//                        executeCommand(receiveMsg.key, receiveMsg.value);</span><br><span class="line">//                        lastSeq.addAndGet(1); // 标记上次消费的位置</span><br><span class="line">//                        System.out.println(&quot;[ Seq: &quot; + receiveMsg.seq + &quot;, Cmd: &quot; + receiveMsg.key + &quot;, val: &quot; + receiveMsg.value + &quot; ]&quot;);</span><br><span class="line">//                    &#125; catch (InterruptedException e) &#123;</span><br><span class="line">//                        e.printStackTrace();</span><br><span class="line">//                    &#125;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">        &#125;).start();</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>下面就是暴力发包的效果，即使丢了【1、2、2、3、3】消息，也可以看到后面按顺序执行了每一条指令，而且只执行了一次。</p><p><img src="https://s4.ax1x.com/2022/01/13/7MPnZ4.png"></p><p>最终结果</p><p>以上，就是对不稳定消息服务的改造全部内容~其中许多地方写的不够严谨，例如客户端重连后，Seq会重置，比较重点不是在这里嘛，而且解决这个问题应该也不是太复杂，大家可以搞一下嘛。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;背景：最近的项目中，有一个端到端控制的场景（即A发送命令，通过RTC服务，使B接收到消息并执行命令），其中为了开发方便使用了RTC服务作为两端通信的“桥梁“。每一组端，都连接到同一个RTC Room，这样一来，多组端都会互不影响。&lt;/p&gt;
&lt;/bl</summary>
      
    
    
    
    <category term="Software" scheme="https://blog.lebear.top/categories/Software/"/>
    
    <category term="Java" scheme="https://blog.lebear.top/categories/Java/"/>
    
    
    <category term="java" scheme="https://blog.lebear.top/tags/java/"/>
    
    <category term="TCP" scheme="https://blog.lebear.top/tags/TCP/"/>
    
    <category term="WebRTC" scheme="https://blog.lebear.top/tags/WebRTC/"/>
    
    <category term="消息队列" scheme="https://blog.lebear.top/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"/>
    
  </entry>
  
  <entry>
    <title>Red Dead Redemption 2:  永远的亚瑟 · 摩根</title>
    <link href="https://blog.lebear.top/2021/02/09/427/"/>
    <id>https://blog.lebear.top/2021/02/09/427/</id>
    <published>2021-02-09T03:45:54.000Z</published>
    <updated>2023-04-09T08:28:53.282Z</updated>
    
    <content type="html"><![CDATA[<p><strong>Red Dead Redemption 2</strong> 是一个致敬西部牛仔世界的一部作品，属于慢热型、注重剧情、注重细节的类型的游戏，我个人愿称之为神作（之一）。</p><h2><span id="剧情">剧情</span></h2><p>首先，想说一下剧情，因为通关后，回味这个剧情实在是太完美了，这个游戏是买完后玩了开头一章，然后一直没时间，隔了半年后一口气通关了。剧情相当棒，但是属于慢热型游戏，如果你对西部题材不感兴趣，很可能前期坚持不下去，但是如果你喜欢跌宕起伏的剧情，那么坚持到后面你会发现新天地！通过剧情，你可以了解到西部世界的魅力，感受到剧情中透露出的人情味，很美好，也很残酷。</p><p><img src="https://s3.ax1x.com/2021/02/10/ywSxTx.jpg"></p><p>前期的亚瑟等人普通帮派谋生，有抢劫，有杀人，还四处逃窜，确实没什么亮点，等到下了雪山，刚开始也很慢热，因为帮派人太多了，你不可能一下子记住那么多人，所以无法很快带入游戏，真正带入游戏的时间大概在第三章开始，慢慢你了解帮派的成员了，通过之前的剧情，也慢慢了解每个人的性格，比如可靠的兰尼、讲义气的查尔斯、女战神沙迪等，就会更加带入剧情了；这个时候你能通过亚瑟的语气，感受到亚瑟的心情，就像自己的心情，真的很有人情味。</p><p>除此之外，每一个NPC，如果你仔细阅读，都会发现，他们都是有血有肉的人，都有着自己的故事。</p><hr><p>在后期亚瑟病逝前，高荣誉下的亚瑟返回营地时候，BGM响起，脑海中浮现出他人对自己一生的评价，当时瞬间泪目，有一种回顾一生善恶的感觉，相当有代入感。</p><p><img src="https://s3.ax1x.com/2021/02/10/ywpru9.jpg"></p><h2><span id="画面">画面</span></h2><p>因为笔者体验游戏是使用1050显卡，没能开最高画质（以后上30系列一定最高画质二刷）。就目前体验，适当配置下是40帧左右，画质很棒，天气效果也很不错。</p><p><img src="https://s3.ax1x.com/2021/02/10/ywSbpF.jpg"></p><hr><p>RDR2虽然看人物动作，还是用着GTA5的引擎，但是画质有了很大提升。能看到例如车辙、马蹄印等细节的变化。</p><p>还能体验到日出、日落、晚霞、星宿的魅力。</p><p><img src="https://s3.ax1x.com/2021/02/10/ywSv01.jpg"></p><hr><p>还有作品中用到了动态天气变化，不再是GTA5中的切换场景变换天气的机制，你能够体验到暴风雨前夕的压抑深厚的云层变幻莫测</p><p><img src="https://s3.ax1x.com/2021/02/10/ywSL6J.jpg"></p><p><img src="https://s3.ax1x.com/2021/02/10/ywSOX9.jpg"></p><p>也能感受到雷雨天气中逼真的闪电效果，还能身临其境的感受到下雪的冬日严寒。</p><p><img src="https://s3.ax1x.com/2021/02/10/ywSjmR.jpg"></p><h2><span id="音乐">音乐</span></h2><p>音乐方面，在平时游玩中，不同地方会无感觉的切换不同的西部风格BGM，很优美；在特定任务中，又会有能让你身临其境的音乐，比如上面提到的地方（亚瑟回顾一生善恶，他人对自己的评价时候的BGM），真的很催泪，能让人产生共鸣的剧情才能称得上优秀的剧情，这里对GTA6抱有很大希望。</p><p><img src="https://s3.ax1x.com/2021/02/10/ywSql4.jpg"></p><h2><span id="操作amp游戏性">操作&amp;游戏性</span></h2><p><strong>射击、移动：</strong> 这个继承了GTA5的特点，包括按键和打击手感，只要习惯GTA5的玩家也会习惯RDR2，但是有一点：骑马是默认按E，如果不小心按了F，等着你的马踢你吧！wwwww</p><hr><p><strong>钓鱼&amp;采集制作：</strong> 这部作品里面，添加了类似RPG游戏的制作，但是恰到好处，没有很复杂；钓鱼也很休闲。</p><hr><p><strong>任务：</strong> 任务是像GTA模式的1-3主线，少量支线组成的，基本属于线性的剧情流程，很适合那些想要体验剧情的玩家。</p><h2><span id="其他">其他</span></h2><p>游戏中有许多彩蛋，这个可以自行在B站搜索。这里只说一个，游戏过程中，不同时期都会遇到一个寻找朋友的人，你在绑架他后，搜身，会发现一封信，如果你仔细读过，不得不令人佩服，R星能把每个人刻画的栩栩如生，你能从信件中了解这个人的一生。还有在某次击杀士兵后，也会发现他的妻子等着他回去的一封信，真的是眼睛里进沙子了。</p><hr><p>游戏中也有致敬荒野大镖客电影的情节，比如一些比手速的拔枪杀人的场面（ 当然主角有子弹时间，必然胜呀）</p><hr><p>游戏中有许多致敬近科学的地方，比如 电椅、遥控船、引雷电为动能的机器人，其中的科学家大多都献身给自己钟爱的科学实验，致敬。</p><hr><p>游戏中继承了GTA5的多角色，其中是双角色（亚瑟死亡后，控制约翰）</p><hr><p>游戏中添加了自动寻路功能，可以让玩家更加专注剧情。</p><h2><span id="ps">ps</span></h2><p>游戏中还有太多的玩法创意和热爱之处，但由于时间和篇幅，就分享到这里。</p><p>最后，让我们致敬永远的 <strong>亚瑟 · 摩根（<em>Arthur</em> _Morgan_）</strong></p><p><img src="https://s3.ax1x.com/2021/02/10/ywpBjJ.jpg"></p><hr><p>笔者steam评价原文：<a href="https://steamcommunity.com/id/longyijason/recommended/1174180/">https://steamcommunity.com/id/longyijason/recommended/1174180/</a></p><p>注：图片内容全部来自Steam社区。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;strong&gt;Red Dead Redemption 2&lt;/strong&gt; 是一个致敬西部牛仔世界的一部作品，属于慢热型、注重剧情、注重细节的类型的游戏，我个人愿称之为神作（之一）。&lt;/p&gt;
&lt;h2&gt;&lt;span id=&quot;剧情&quot;&gt;剧情&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;首先，想说</summary>
      
    
    
    
    <category term="游戏" scheme="https://blog.lebear.top/categories/%E6%B8%B8%E6%88%8F/"/>
    
    
    <category term="Red Dead Redemption 2" scheme="https://blog.lebear.top/tags/Red-Dead-Redemption-2/"/>
    
    <category term="steam" scheme="https://blog.lebear.top/tags/steam/"/>
    
    <category term="亚瑟" scheme="https://blog.lebear.top/tags/%E4%BA%9A%E7%91%9F/"/>
    
    <category term="游戏" scheme="https://blog.lebear.top/tags/%E6%B8%B8%E6%88%8F/"/>
    
    <category term="荒野大镖客" scheme="https://blog.lebear.top/tags/%E8%8D%92%E9%87%8E%E5%A4%A7%E9%95%96%E5%AE%A2/"/>
    
  </entry>
  
  <entry>
    <title>分享如何低成本DIY组装一台显示器</title>
    <link href="https://blog.lebear.top/2021/01/17/367/"/>
    <id>https://blog.lebear.top/2021/01/17/367/</id>
    <published>2021-01-17T14:07:05.000Z</published>
    <updated>2023-04-09T08:28:53.282Z</updated>
    
    <content type="html"><![CDATA[<h2><span id="一-写在前面">一、写在前面</span></h2><p>相信有许多动手能力强的朋友，你是否想组装一台属于自己的独一无二的显示器呢？笔者下面分享一下Diy显示器的过程。文中也有一些对于显示器参数的拙见以及显示器最终的测试方式。</p><p><img src="/2021/01/image-14.png"></p><p>转载自酷安</p><p><img src="/2021/01/image-15.png"></p><p>转载自酷安</p><p><img src="https://s3.ax1x.com/2021/01/18/sy6tr6.jpg"></p><p>笔者DIY显示器成品</p><h2><span id="二-首先了解diy显示器都需要准备哪些部件">二、首先了解DIY显示器都需要准备哪些部件</span></h2><ul><li>液晶屏面板</li><li>驱动板</li><li>外壳</li><li>HDMI&#x2F;DP&#x2F;VGA线</li><li>电源线</li></ul><h2><span id="三-选购液晶屏面板">三、选购液晶屏面板</span></h2><p><img src="/2021/01/image-5.png"></p><p>[su_highlight]流程：选择分辨率（1080P） —— 选择屏幕大小（21.5‘）—— 选择屏幕类型（IPS）—— 选择品牌 —— 某宝&#x2F;某东选购[&#x2F;su_highlight]</p><p>[su_box title&#x3D;”科普” style&#x3D;”soft” box_color&#x3D;”#8a0014” radius&#x3D;”5”]首先补习一下屏幕相关知识：  </p><p>IPS屏和TN屏？<br>IPS屏：直观感受柔和、色彩更精准、可视角度大（在侧面看屏幕依然不损失太多色彩）<br>TN屏：响应时间相比同水平IPS屏幕低（电竞屏幕常用），更容易做到高刷新率，但是可视角度略小于同水平IPS屏幕（在侧面看会偏白）<br>8bit？10bit？<br>8bit表示每个原色具有256个灰阶,即0-255对应色彩从黑到白的灰度级别,10bit表示单色彩通道具有1024个灰度级别,色阶范围是0-1023。还有一种叫做“8抖10bit”，原生8bit，通过闪烁颜色强行模拟出来10bit的色数。这个当然是越高越好，越高则越能体现出色彩的渐变。<br>色域?<br>对于设计使用的显示器要求严格，有三个标准：sRGB、NTSC、Adobe RGB，覆盖越全面越好，说明显示器可以表现的色彩就越丰富。<br>除此之外，刷新率（屏幕每秒刷新的次数），分辨率（像素点的数量）这些常见参数相比大家已经非常清晰，就不做赘述了。 [&#x2F;su_box]</p><p>这里通过屏库网[su_button url&#x3D;”<a href="https://jingyan.baidu.com/article/6dad5075d1dc40a123e36ea3.html&quot;">https://jingyan.baidu.com/article/6dad5075d1dc40a123e36ea3.html&quot;</a> target&#x3D;”blank” style&#x3D;”bubbles” size&#x3D;”4” radius&#x3D;”round” icon&#x3D;”icon: link” text_shadow&#x3D;”0px 0px 0px #000000”]访问屏库网[&#x2F;su_button]来查找选取面板。</p><p>因为笔者短时间使用该屏幕，所以就只选择了1080P的屏幕，这里笔者通过 1920*1080，21.5英寸，IPS屏来筛选到以下内容。</p><p><img src="/2021/01/image.png"></p><p>这里我们根据自己的需求，可以查找2K，4K等其他不同分辨率规格的屏幕，请有能力的同学一定选取大厂的屏幕，比如LG、三星，当然价钱也会稍微贵一些，但质量有保证。</p><p><img src="/2021/01/image-1.png"></p><p>接着点进去，我们可以看到详细的显示器参数，如果我们是选购笔记本显示器，那么请注意接口是多少Pin的，要和你笔记本支持的相匹配（当然我的笔记本也换过屏幕，说起来全是泪QAQ），这里没问题后，到电商平台搜索相关型号（比如<a href="https://www.panelook.cn/HR215WU1-210_BOE_21.5_LCM_overview_cn_21409.html"><strong>HR215WU1-210</strong></a>）</p><p><img src="/2021/01/image-2.png"></p><p>一般这里随便选取一家等级高的店买就好，一般不会翻车（大多便宜的屏幕都是拆机屏，一般没大问题），买前先和店主沟通，说出你要的型号，然后差不多下单即可。</p><h2><span id="四-驱动板">四、驱动板</span></h2><p><img src="/2021/01/image-4.png"></p><p>我们有了显示器。但是如果要连接你的笔记本or你的显卡，还需要HDMI&#x2F;VGA&#x2F;DP线。至于这个线插在哪里，就需要考虑到驱动板，驱动板顾名思义就是驱动我们的屏幕的电路板，是一块屏幕的大脑，当然这个大脑成本不高所以也不是很贵。</p><p>驱动板我们也可以在某宝搜索购买，同样找到后，联系客服，说明显示器面板型号，他会推荐给我们响应的驱动板。</p><p><img src="/2021/01/image-3.png"></p><h2><span id="五-外壳">五、外壳</span></h2><p>两种方案：微积木（可以个性化拼装、加灯、但是费时费力）、直接买显示器外壳（普通实用）</p><h3><span id="1-微积木">1、微积木</span></h3><p>某宝或者1688搜索微积木选便宜的购买，我当时是购买了5000颗微积木，拼显示器外壳</p><p><img src="/2021/01/image-6.png"></p><p><img src="https://s3.ax1x.com/2021/01/18/sy6OdU.jpg"></p><h3><span id="2-直接购买外壳">2、直接购买外壳</span></h3><p>从某宝，搜索显示器外壳，然后联系商家，说明显示器大小参数，商家会给你推荐合适的商品，或者从咸鱼买别人坏了的显示器的外壳。</p><h2><span id="六-hdmix2fdpx2fvga线">六、HDMI&#x2F;DP&#x2F;VGA线</span></h2><p>这些线都是视频输出的线，显卡输出到显示器则需要有线连接。一般便宜的显示器驱动板，具有HDMI口和VGA口，VGA适用于稍微老一点的PC，HDMI高清线为比较常用的线，DP则为更高端的具有更高带宽的线。根据驱动板的接口来选择</p><p>HDMI</p><p><img src="/2021/01/image-11.png"></p><p>VGA</p><p><img src="/2021/01/image-8.png"></p><p>DP</p><p><img src="/2021/01/image-10.png"></p><p>如下图为驱动板，你需要做的就是购买你驱动板和显卡（or笔记本）同时支持的接口的线即可。</p><p><img src="/2021/01/image-4.png"></p><h2><span id="七-电源线">七、电源线</span></h2><p>买显示器电源线即可，一般12V 2A即可，如果不清楚，可以在某宝搜索后，问商家。</p><p><img src="/2021/01/image-13.png"></p><h2><span id="八-安装-测试">八、安装、测试</span></h2><h3><span id="1-安装">1、安装</span></h3><p><img src="https://s3.ax1x.com/2021/01/18/sycL6I.png"></p><p><img src="https://s3.ax1x.com/2021/01/18/sygcE8.png"></p><p>最后，再连接HDMI线（驱动板与笔记本或显卡连接），点亮，下图为连接笔记本后点亮成功。</p><p><img src="https://s3.ax1x.com/2021/01/18/syroxx.jpg"></p><h2><span id="2-测试">2、测试</span></h2><h4><span id="1-使用displayx工具">1、使用DisplayX工具</span></h4><p>DisplayX 下载地址：<a href="https://wws.lanzous.com/ikgRKkix8ji">https://wws.lanzous.com/ikgRKkix8ji</a> [su_button url&#x3D;”<a href="https://wws.lanzous.com/ikgRKkix8ji&quot;">https://wws.lanzous.com/ikgRKkix8ji&quot;</a> target&#x3D;”blank” background&#x3D;”#94170b” radius&#x3D;”round” icon&#x3D;”icon: link” text_shadow&#x3D;”0px 0px 0px #000000”]点击访问[&#x2F;su_button]</p><p>中文界面，里面的功能一目了然，大家可以使用该工具根据中文提示来测试显示器质量。</p><p><img src="/2021/01/image-17.png"></p><h4><span id="2-刷新率测试">2、刷新率测试</span></h4><p>UFO Test：<a href="https://www.testufo.com/">https://www.testufo.com/</a> [su_button url&#x3D;”<a href="https://www.testufo.com/&quot;">https://www.testufo.com/&quot;</a> target&#x3D;”blank” background&#x3D;”#94170b” radius&#x3D;”round” icon&#x3D;”icon: link” text_shadow&#x3D;”0px 0px 0px #000000”]点击访问[&#x2F;su_button]</p><p><img src="/2021/01/image-18.png"></p><h2><span id="九-写在最后">九、写在最后</span></h2><p>曾经我以为，能坚持做一件事是因为这件事是好的；现在才发现，而是因为喜欢才能让自己坚持下去。喜欢技术，喜欢尝试，喜欢动手，这就是我。后续，还会出一篇关于路由器刷机的文章。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2&gt;&lt;span id=&quot;一-写在前面&quot;&gt;一、写在前面&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;相信有许多动手能力强的朋友，你是否想组装一台属于自己的独一无二的显示器呢？笔者下面分享一下Diy显示器的过程。文中也有一些对于显示器参数的拙见以及显示器最终的测试方式。&lt;/p&gt;
&lt;p&gt;&lt;img </summary>
      
    
    
    
    <category term="数码" scheme="https://blog.lebear.top/categories/%E6%95%B0%E7%A0%81/"/>
    
    
    <category term="DIY" scheme="https://blog.lebear.top/tags/DIY/"/>
    
    <category term="HDMI" scheme="https://blog.lebear.top/tags/HDMI/"/>
    
    <category term="刷新率" scheme="https://blog.lebear.top/tags/%E5%88%B7%E6%96%B0%E7%8E%87/"/>
    
    <category term="数码" scheme="https://blog.lebear.top/tags/%E6%95%B0%E7%A0%81/"/>
    
    <category term="显示器" scheme="https://blog.lebear.top/tags/%E6%98%BE%E7%A4%BA%E5%99%A8/"/>
    
    <category term="计算机" scheme="https://blog.lebear.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA/"/>
    
  </entry>
  
  <entry>
    <title>解决使用jedis连接是报DENIED Redis is running in protected mode错误</title>
    <link href="https://blog.lebear.top/2020/12/16/342/"/>
    <id>https://blog.lebear.top/2020/12/16/342/</id>
    <published>2020-12-16T14:54:04.000Z</published>
    <updated>2023-04-09T08:28:53.284Z</updated>
    
    <content type="html"><![CDATA[<p><code>DENIED Redis is running</code> <code>in</code> <code>protected mode because protected mode is enabled, no bind address was specified, &lt;br&gt;no authentication password is requested to clients. In this mode connections are only accepted from the loopback interface.&lt;br&gt; If you want to connect from external computers to Redis you may adopt one of the following solutions:``1) Just disable protected mode sending the</code> <code>command</code> <code>&#39;CONFIG SET protected-mode no&#39;</code> <code>from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet</code> <code>if</code> <code>you</code> <code>do</code> <code>so. Use CONFIG REWRITE to</code> <code>make</code> <code>this change permanent.``2) Alternatively you can just disable the protected mode by editing the Redis configuration</code> <code>file``, and setting the protected mode option to</code> <code>&#39;no&#39;``, and</code> <code>then</code> <code>restarting the server.``3) If you started the server manually just</code> <code>for</code> <code>testing, restart it with the</code> <code>&#39;--protected-mode no&#39;</code> <code>option.``4) Setup a bind address or an authentication password. NOTE: You only need to</code> <code>do</code> <code>one of the above things</code> <code>in</code> <code>order</code> <code>for</code> <code>the server to start accepting connections from the outside.</code></p><p>1. 错误原因：由于redis的保护模式开启了，并且没有绑定ip地址，没有密码认证</p><p>2. 解决方案<br>方案一：</p><ol><li>使用 设置CONFIG SET protected-mode no</li></ol><p>步骤：</p><p>1. 在redis本机上打开redis-cli工具，并连接</p><p>2. 在其中输入CONFIG SET protected-mode no即可</p><p>缺点：</p><p>该种方式只是短暂性有效，如果redisServer重新启动后还是不能正常链接</p><p>方案二：</p><p>1)编辑redis配置文件，将保护模式关闭</p><p>步骤：</p><p>由于我是直接使用redis-server 命令启动的，所以说其服务是按照其默认设置进行启动，修改&#x2F;usr&#x2F;local&#x2F;etc&#x2F;redis.conf</p><p>如果没有请从下载的redis中复制一个redis.conf到这里，</p><p><a href="https://images2018.cnblogs.com/blog/917948/201805/917948-20180524201313844-172144694.png"><img src="https://images2018.cnblogs.com/blog/917948/201805/917948-20180524201313844-172144694.png"></a></p><p>将bind 后面绑定的ip后面加入你本机外网的ip</p><p>1</p><p><code>bind 127.0.0.1 192.168.21.4</code></p><p>然后保存，再使用redis-server &#x2F;usr&#x2F;local&#x2F;etc&#x2F;redis.conf启动</p><p>问题：</p><p>　　1.redis-cli不能直接使用，必须输入绑定的ip+d端口号才能正常使用</p><p>　　2.不安全</p><p>2). 将redis.conf中安全模式关闭</p><p><a href="https://images2018.cnblogs.com/blog/917948/201805/917948-20180524203128935-489749165.png"><img src="https://images2018.cnblogs.com/blog/917948/201805/917948-20180524203128935-489749165.png"></a></p><p>将该保护模式改为no</p><p>再试</p><p>缺点：1.保护模式关闭后不安全</p><p>3)设置密码访问模式</p><p>在redis.conf中加入一行密码设置</p><p>1</p><p><code>require 你的密码</code></p><p> 保存，再重新启动redis客户端</p><p>以上四种方式解决该错误都可以，但是根据上面的优缺点，</p><p><strong>选择第四种方式最佳，安全性最高</strong></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;code&gt;DENIED Redis is running&lt;/code&gt; &lt;code&gt;in&lt;/code&gt; &lt;code&gt;protected mode because protected mode is enabled, no bind address was specifie</summary>
      
    
    
    
    <category term="uncategorized" scheme="https://blog.lebear.top/categories/uncategorized/"/>
    
    
  </entry>
  
  <entry>
    <title>OBS录制全屏黑屏解决方法</title>
    <link href="https://blog.lebear.top/2020/10/20/290/"/>
    <id>https://blog.lebear.top/2020/10/20/290/</id>
    <published>2020-10-19T16:36:02.000Z</published>
    <updated>2023-04-09T08:28:53.281Z</updated>
    
    <content type="html"><![CDATA[<p>问题描述：在使用OBS进行录屏时候，只能录制特定窗口，当选屏幕录制时候，确实黑屏。</p><p>原因：obs默认只录制和自己用同显卡的程序，默认obs是用独显，所以运行游戏一般都可以录制，但是屏幕录制却不行，就是这个原因。</p><p>1、Nvidia控制面板，使用核显运行（如果还不行，则尝试第二种方法）<br>2、在设置-系统-显示-图形设置，使obs为核显运行  </p><p><img src="/2020/10/image-1.png"></p><p><img src="/2020/10/image-2.png"></p><p><img src="/2020/10/image-3.png"></p><p>使用浏览，添加obs.exe</p><p><img src="/2020/10/image-4.png"></p><p>设置其使用核显，这里是节能模式默认使用核显</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;问题描述：在使用OBS进行录屏时候，只能录制特定窗口，当选屏幕录制时候，确实黑屏。&lt;/p&gt;
&lt;p&gt;原因：obs默认只录制和自己用同显卡的程序，默认obs是用独显，所以运行游戏一般都可以录制，但是屏幕录制却不行，就是这个原因。&lt;/p&gt;
&lt;p&gt;1、Nvidia控制面板，使用核显</summary>
      
    
    
    
    <category term="Software" scheme="https://blog.lebear.top/categories/Software/"/>
    
    
    <category term="OBS" scheme="https://blog.lebear.top/tags/OBS/"/>
    
    <category term="软件" scheme="https://blog.lebear.top/tags/%E8%BD%AF%E4%BB%B6/"/>
    
  </entry>
  
  <entry>
    <title>浅谈ArrayList源码实现</title>
    <link href="https://blog.lebear.top/2020/10/20/296/"/>
    <id>https://blog.lebear.top/2020/10/20/296/</id>
    <published>2020-10-19T16:19:26.000Z</published>
    <updated>2023-04-09T08:28:53.283Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文记录笔者对JDK1.8下ArrayList源码的解读</p></blockquote><h2><span id="arraylist中属性解读">ArrayList中属性解读</span></h2><ul><li>transient Object[] elementData; &#x2F;&#x2F;顾名思义，属性为数据空间，有容量大小限制，其中存放add进的数据</li><li>private static final Object[] EMPTY_ELEMENTDATA &#x3D; {};&#x2F;&#x2F;如果在调用构造方法传入初始容量为0时候，会创建一个空elementData</li><li>private int size;&#x2F;&#x2F;该属性为List中拥有的元素个数</li></ul><h2><span id="add添加元素">add：添加元素</span></h2><h3><span id="boolean-adde-e">boolean add(E e)</span></h3><p>此方法为单独插入元素到List中</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">public boolean add(E e) &#123;</span><br><span class="line">    ensureCapacityInternal(size + 1);  // 确保新元素可添加的条件为容量大于等于size+1</span><br><span class="line">    elementData[size++] = e;//在数组尾部添加元素，顺便size+1</span><br><span class="line">    return true;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3><span id="void-addint-index-e-element">void add(int index, E element)</span></h3><p>此方法为在index处插入元素element</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">public void add(int index, E element) &#123;</span><br><span class="line">        rangeCheckForAdd(index);//数组越界Check，条件：index &gt; size  index &lt; 0</span><br><span class="line"></span><br><span class="line">        ensureCapacityInternal(size + 1);  //这句为了确保要插入数据容量至少大于size+1，内部实现中如果minCapacity - elementData.length &gt; 0会调用grow方法扩容。</span><br><span class="line">        System.arraycopy(elementData, index, elementData, index + 1,</span><br><span class="line">                         size - index);//arraycopy是一个native方法，作用是将要插入位置后的所有元素后移一位，在index处预留出空间以供新元素插入</span><br><span class="line">        elementData[index] = element;//把预插入元素放置到index位置</span><br><span class="line">        size++;//list大小增加</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2><span id="grow扩容">grow：扩容</span></h2><p>此方法在add前会进行容量检测，可能调用扩容方法</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">    private void grow(int minCapacity) &#123;//传参为最小需要扩容的大小</span><br><span class="line">        // overflow-conscious code</span><br><span class="line">        int oldCapacity = elementData.length;//原有容量</span><br><span class="line">        int newCapacity = oldCapacity + (oldCapacity &gt;&gt; 1);//新容量为原容量的1.5倍</span><br><span class="line">        if (newCapacity - minCapacity &lt; 0)//如果新容量（1.5倍）小于最小需要容量（原因：int取值范围越界）</span><br><span class="line">            newCapacity = minCapacity;//赋值为最小所需容量</span><br><span class="line">        if (newCapacity - MAX_ARRAY_SIZE &gt; 0)//如果新容量大于int最大值-8</span><br><span class="line">            newCapacity = hugeCapacity(minCapacity);//则利用hugeCapacity进行计算复制大小（后面有写）</span><br><span class="line">        // minCapacity is usually close to size, so this is a win:</span><br><span class="line">        elementData = Arrays.copyOf(elementData, newCapacity);//分配好新空间，可以对旧数组进行扩容</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    private static int hugeCapacity(int minCapacity) &#123;</span><br><span class="line">        if (minCapacity &lt; 0) // overflow 溢出</span><br><span class="line">            throw new OutOfMemoryError();</span><br><span class="line">        return (minCapacity &gt; MAX_ARRAY_SIZE) ?</span><br><span class="line">            Integer.MAX_VALUE ://最小分配大小大于Integer_Max-8则复制为int最大值</span><br><span class="line">            MAX_ARRAY_SIZE;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">//此外，如ensureCapacity等方法，内部都是调用grow来进行扩容，没什么其他内容，这里就不谈了</span><br></pre></td></tr></table></figure><h2><span id="remove移除元素">remove：移除元素</span></h2><h3><span id="public-e-removeint-index按index移除">public E remove(int index)：按index移除</span></h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">public E remove(int index) &#123;</span><br><span class="line">    rangeCheck(index);//检查index越界</span><br><span class="line"></span><br><span class="line">    modCount++;//标记修改次数，类似版本号</span><br><span class="line">    E oldValue = elementData(index);//取出index位置的元素</span><br><span class="line"></span><br><span class="line">    int numMoved = size - index - 1;//需要移动元素的个数，也就是如果从index位置删除元素，那么后续所有其他元素需要前移一位来补空缺</span><br><span class="line">    if (numMoved &gt; 0)//如果移动的元素不是最后一个元素，则都需要前移后面所有元素，由此可见，如果移除index=0的元素的效率最差，需要数组整体前移</span><br><span class="line">        System.arraycopy(elementData, index+1, elementData, index,</span><br><span class="line">                         numMoved);//移动操作</span><br><span class="line">    elementData[--size] = null; // clear to let GC do its work，将最后一个元素位置置为null，原注释解释方便GC回收垃圾</span><br><span class="line"></span><br><span class="line">    return oldValue;//返回删除的元素</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3><span id="public-boolean-removeobject-o按元素移除">public boolean remove(Object o)：按元素移除</span></h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">public boolean remove(Object o) &#123;</span><br><span class="line">        if (o == null) &#123;//如果要移除元素为null</span><br><span class="line">            for (int index = 0; index &lt; size; index++)</span><br><span class="line">                if (elementData[index] == null) &#123;遍历查找数值=null的元素</span><br><span class="line">                    fastRemove(index);//实际就是封装了之前移除过程</span><br><span class="line">                    return true;</span><br><span class="line">                &#125;</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            for (int index = 0; index &lt; size; index++)</span><br><span class="line">                if (o.equals(elementData[index])) &#123;//使用equals来进行比较，如果相等，则移除</span><br><span class="line">                    fastRemove(index);</span><br><span class="line">                    return true;</span><br><span class="line">                &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        return false;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">//此方法基本上就是对于之前移除过程进行封装，可参考public E remove(int index)中的移除注释</span><br><span class="line">private void fastRemove(int index) &#123;</span><br><span class="line">        modCount++;</span><br><span class="line">        int numMoved = size - index - 1;</span><br><span class="line">        if (numMoved &gt; 0)</span><br><span class="line">            System.arraycopy(elementData, index+1, elementData, index,</span><br><span class="line">                             numMoved);</span><br><span class="line">        elementData[--size] = null; // clear to let GC do its work</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h2><span id="iterator内部实现迭代器">Iterator：内部实现迭代器</span></h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br></pre></td><td class="code"><pre><span class="line">public Iterator&lt;E&gt; iterator() &#123;//默认调用此方法</span><br><span class="line">            return listIterator();//其中会调用listIterator(0)，也就是从0位置开始迭代</span><br><span class="line">&#125;        </span><br><span class="line"></span><br><span class="line">public ListIterator&lt;E&gt; listIterator(final int index) &#123;</span><br><span class="line">            checkForComodification();//检测修改版本号，for concurrent</span><br><span class="line">            rangeCheckForAdd(index);//检测越界</span><br><span class="line">            final int offset = this.offset;</span><br><span class="line"></span><br><span class="line">            return new ListIterator&lt;E&gt;() &#123;</span><br><span class="line">                int cursor = index;//内部维护索引，默认为0开始</span><br><span class="line">                int lastRet = -1;//记录上一次返回的索引，初始化为-1</span><br><span class="line">                int expectedModCount = ArrayList.this.modCount;</span><br><span class="line"></span><br><span class="line">                public boolean hasNext() &#123;//是否有下一个元素</span><br><span class="line">//这里使用sublist，实际上内部有属性指向parent(当前list)，可理解为就是当前list</span><br><span class="line">                    return cursor != SubList.this.size;//判断游标是否到达list的size</span><br><span class="line"></span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                @SuppressWarnings(&quot;unchecked&quot;)</span><br><span class="line">                public E next() &#123;</span><br><span class="line">                    checkForComodification();//检查修改次数版本号</span><br><span class="line">                    int i = cursor;</span><br><span class="line">                    if (i &gt;= SubList.this.size)//如果越界，抛出无元素异常</span><br><span class="line">                        throw new NoSuchElementException();</span><br><span class="line">                    Object[] elementData = ArrayList.this.elementData;//创建对list的引用</span><br><span class="line">                    if (offset + i &gt;= elementData.length)//越界</span><br><span class="line">                        throw new ConcurrentModificationException();</span><br><span class="line">                    cursor = i + 1;//此时游标自动后移，为了迭代继续进行</span><br><span class="line">                    return (E) elementData[offset + (lastRet = i)];//返回通过游标取出的元素，更新lastRet为当前返回的索引</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                public boolean hasPrevious() &#123;//如果游标不等于0，则有前置元素</span><br><span class="line">                    return cursor != 0;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                @SuppressWarnings(&quot;unchecked&quot;)</span><br><span class="line">                public E previous() &#123;//返回前置元素，与next方法大同小异</span><br><span class="line">                    checkForComodification();</span><br><span class="line">                    int i = cursor - 1;</span><br><span class="line">                    if (i &lt; 0)</span><br><span class="line">                        throw new NoSuchElementException();</span><br><span class="line">                    Object[] elementData = ArrayList.this.elementData;</span><br><span class="line">                    if (offset + i &gt;= elementData.length)</span><br><span class="line">                        throw new ConcurrentModificationException();</span><br><span class="line">                    cursor = i;</span><br><span class="line">                    return (E) elementData[offset + (lastRet = i)];</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                @SuppressWarnings(&quot;unchecked&quot;)//支持lambda表达式特性的方法</span><br><span class="line">                public void forEachRemaining(Consumer&lt;? super E&gt; consumer) &#123;</span><br><span class="line">                    Objects.requireNonNull(consumer);</span><br><span class="line">                    final int size = SubList.this.size;</span><br><span class="line">                    int i = cursor;</span><br><span class="line">                    if (i &gt;= size) &#123;</span><br><span class="line">                        return;</span><br><span class="line">                    &#125;</span><br><span class="line">                    final Object[] elementData = ArrayList.this.elementData;</span><br><span class="line">                    if (offset + i &gt;= elementData.length) &#123;</span><br><span class="line">                        throw new ConcurrentModificationException();</span><br><span class="line">                    &#125;</span><br><span class="line">                    while (i != size &amp;&amp; modCount == expectedModCount) &#123;</span><br><span class="line">                        consumer.accept((E) elementData[offset + (i++)]);</span><br><span class="line">                    &#125;</span><br><span class="line">                    // update once at end of iteration to reduce heap write traffic</span><br><span class="line">                    lastRet = cursor = i;</span><br><span class="line">                    checkForComodification();</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                public int nextIndex() &#123;//返回游标下一次访问的元素位置</span><br><span class="line">                    return cursor;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                public int previousIndex() &#123;//返回上一个访问的位置</span><br><span class="line">                    return cursor - 1;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                public void remove() &#123;//移除元素</span><br><span class="line">                    if (lastRet &lt; 0)</span><br><span class="line">                        throw new IllegalStateException();</span><br><span class="line">                    checkForComodification();//同上</span><br><span class="line"></span><br><span class="line">                    try &#123;</span><br><span class="line">                        SubList.this.remove(lastRet);//内部调用此list的remove方法去移除刚访问过的元素</span><br><span class="line">                        cursor = lastRet;//移除后，内部游标则跳到上一次访问元素位置</span><br><span class="line">                        lastRet = -1;//上一次访问元素因为被删除，所以lastret又置为-1</span><br><span class="line">                        expectedModCount = ArrayList.this.modCount;</span><br><span class="line">                    &#125; catch (IndexOutOfBoundsException ex) &#123;</span><br><span class="line">                        throw new ConcurrentModificationException();</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                public void set(E e) &#123;</span><br><span class="line">                    if (lastRet &lt; 0)</span><br><span class="line">                        throw new IllegalStateException();</span><br><span class="line">                    checkForComodification();//修改版本检测</span><br><span class="line"></span><br><span class="line">                    try &#123;</span><br><span class="line">                        ArrayList.this.set(offset + lastRet, e);//调用set方法设置当前元素为新值</span><br><span class="line">                    &#125; catch (IndexOutOfBoundsException ex) &#123;</span><br><span class="line">                        throw new ConcurrentModificationException();</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                public void add(E e) &#123;</span><br><span class="line">                    checkForComodification();</span><br><span class="line"></span><br><span class="line">                    try &#123;</span><br><span class="line">                        int i = cursor;</span><br><span class="line">                        SubList.this.add(i, e);//在当前游标位置添加元素</span><br><span class="line">                        cursor = i + 1;//游标后移一位</span><br><span class="line">                        lastRet = -1;</span><br><span class="line">                        expectedModCount = ArrayList.this.modCount;//设置期望的修改版本号，此处运用了类似CAS的控制机制，在许多方法调用前会有checkForComodification()方法来进行修改版本号检测</span><br><span class="line">                    &#125; catch (IndexOutOfBoundsException ex) &#123;</span><br><span class="line">                        throw new ConcurrentModificationException();</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">                final void checkForComodification() &#123;//修改版本号检测</span><br><span class="line">                    if (expectedModCount != ArrayList.this.modCount)</span><br><span class="line">//这里使用了期望修改版本号与当前修改版本号对比，来判断是否期间有其他线程修改了这个值，利用CAS机制，以支持并发操作；如果不一致抛出并发修改异常</span><br><span class="line">                        throw new ConcurrentModificationException();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;;</span><br><span class="line">        &#125;</span><br></pre></td></tr></table></figure><h2><span id="get-and-set简单的获取元素和设置元素">get and set：简单的获取元素和设置元素</span></h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"> public E get(int index) &#123;</span><br><span class="line">    rangeCheck(index);//检查越界</span><br><span class="line"></span><br><span class="line">    return elementData(index);//直接返回元素</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">public E set(int index, E element) &#123;</span><br><span class="line">    rangeCheck(index);//检查越界</span><br><span class="line"></span><br><span class="line">    E oldValue = elementData(index);//记录index位置的旧元素</span><br><span class="line">    elementData[index] = element;//将index位置设置为新元素</span><br><span class="line">    return oldValue;//返回旧元素</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2><span id="思考">思考：</span></h2><p>1、由上面源代码，可以总结，许多地方都用到了ensureCapacity与modcount版本号检测方法，如果我们学过设计模式后，考虑是否可以使用一个代理类，来完成一些鲁棒性等内容的前置代理？</p><p>2、对于remove的两个方法，可以明显发现，数组前移一个是写在方法内，另一个则是调用封装方法，所以这一点可以统一使用封装方法，重用代码。</p><p>3、本文未讲解到支持lambda部分的源码，之后笔者会补充。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;本文记录笔者对JDK1.8下ArrayList源码的解读&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;&lt;span id=&quot;arraylist中属性解读&quot;&gt;ArrayList中属性解读&lt;/span&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;transient Ob</summary>
      
    
    
    
    <category term="JDK源码学习" scheme="https://blog.lebear.top/categories/JDK%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0/"/>
    
    
  </entry>
  
  <entry>
    <title>从Android Studio中导出签名APK</title>
    <link href="https://blog.lebear.top/2020/09/07/263/"/>
    <id>https://blog.lebear.top/2020/09/07/263/</id>
    <published>2020-09-07T01:57:43.000Z</published>
    <updated>2023-04-09T08:28:53.283Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><strong>前提：已经根据文档写出HelloWorld代码</strong></p></blockquote><h3><span id="步骤1生成签名文件">步骤1：生成签名文件</span></h3><p>在Android Studio顶栏选则<strong>Build</strong> -&gt; **Generate Signed Bundle &#x2F; APK…**（如图1-1）</p><p><img src="/2020/09/image.png"></p><p>图1-1</p><p>打开出现图1-2，选择APK，点击Next</p><p><img src="/2020/09/image-1.png"></p><p>图1-2</p><p>出现选择密钥界面（如图1-3），这时我们点击<strong>Create new…</strong></p><p><img src="/2020/09/image-2.png"></p><p>图1-3</p><p>如图1-4，选择<strong>密钥存储位置</strong>，填写其他信息，然后<strong>点OK</strong>；</p><p><img src="/2020/09/image-3.png"></p><p>图1-4</p><p>有可能会报错，而且会生成一个android.jks文件（如图1-5、1-6）</p><p><img src="/2020/09/image-4-1024x149.png"></p><p>图1-5  </p><p><img src="/2020/09/image-5.png"></p><p>图1-6</p><p>此时按照提示 打开命令提示符（CMD），输入下面的内容，回车</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">keytool -importkeystore -srckeystore G:\APK\android.jks -destkeystore G:\APK\android.jks -deststoretype pkcs12</span><br></pre></td></tr></table></figure><p>注：如果提示’keytool’ 不是内部或外部命令，也不是可运行的程序<br>或批处理文件。 则是由于没有配置JDK环境变量所致，参考网络教程[su_button url&#x3D;”<a href="https://jingyan.baidu.com/article/6dad5075d1dc40a123e36ea3.html&quot;">https://jingyan.baidu.com/article/6dad5075d1dc40a123e36ea3.html&quot;</a> target&#x3D;”blank” style&#x3D;”bubbles” size&#x3D;”4” radius&#x3D;”round” text_shadow&#x3D;”0px 0px 0px #000000”]JDK环境变量配置指南[&#x2F;su_button]</p><p>此时，以标准方式又创建了一遍<a href="https://www.solvusoft.com/zh-cn/file-extensions/file-extension-jks/">jks加密文件</a>。（图1-7、1-8）</p><p><img src="/2020/09/image-6.png"></p><p>图1-7</p><p><img src="/2020/09/image-7.png"></p><p>图1-8</p><h3><span id="步骤2使用签名文件">步骤2：使用签名文件</span></h3><p>然后再到图1-3处，输入已有的加密文件（jks）路径和口令（如图2-1）</p><p><img src="/2020/09/image-8.png"></p><p>图2-1</p><p>点击Next，选择release版本，勾选V1和V2，否可可能出现jar未签名的失败结果。最后Finish（如图2-2）</p><p><img src="/2020/09/image-9.png"></p><p>图2-2</p><p>这样，我们的签名好的APK就成功导出（如图2-3、2-4）</p><p><img src="/2020/09/image-10.png"></p><p>图2-3</p><p><img src="/2020/09/image-11.png"></p><p>图2-4</p><h3><span id="步骤3验证是否成功签名">步骤3：验证是否成功签名</span></h3><p>命令提示符cd转到apk目录，输入：jarsigner -verbose -certs -verify app-release.apk，回车。</p><p><img src="/2020/09/image-12.png"></p><p>提示如下内容，即签发成功。否则失败，重新尝试吧！</p><p><img src="/2020/09/image-13.png"></p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;前提：已经根据文档写出HelloWorld代码&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;span id=&quot;步骤1生成签名文件&quot;&gt;步骤1：生成签名文件&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;在Android Studio</summary>
      
    
    
    
    <category term="Android" scheme="https://blog.lebear.top/categories/Android/"/>
    
    
    <category term="教程" scheme="https://blog.lebear.top/tags/%E6%95%99%E7%A8%8B/"/>
    
    <category term="Android" scheme="https://blog.lebear.top/tags/Android/"/>
    
    <category term="Android Studio" scheme="https://blog.lebear.top/tags/Android-Studio/"/>
    
    <category term="APK" scheme="https://blog.lebear.top/tags/APK/"/>
    
  </entry>
  
  <entry>
    <title>Python爬虫实战 - 爬取视频网站的下载链接</title>
    <link href="https://blog.lebear.top/2020/07/01/194/"/>
    <id>https://blog.lebear.top/2020/07/01/194/</id>
    <published>2020-06-30T18:41:20.000Z</published>
    <updated>2023-04-09T08:28:53.282Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><strong>什么是爬虫？为什么要使用爬虫？</strong></p><p>爬虫（即 网络爬虫 ）是一种按照一定的规则，自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。 （摘取自百度百科）  </p><p>使用爬虫可以方便地获得某个网页，或者某系列网页一些规则下的内容，由按照规则自动执行地脚本来代替手动查询内容等繁琐工作。  </p></blockquote><p><strong>将要使用到的技术或知识：</strong></p><ul><li><a href="https://www.runoob.com/python/python-tutorial.html">Python基础语法</a></li><li><a href="http://www.jsphp.net/python/show-24-214-1.html">Python库：BeautifulSoup4</a></li><li><a href="url=%22https://www.runoob.com/python3/python-mysql-connector.html">Python库：mysql-connector</a></li><li><a href="https://www.runoob.com/js/js-tutorial.html">JavaScript基础语法</a></li><li><a href="https://www.runoob.com/html/html-tutorial.html">HTML基础语法</a></li></ul><hr><h3><span id="一-第三方库beautiful-soup安装"><strong>一、第三方库Beautiful Soup安装</strong></span></h3><p><strong>Beautiful Soup</strong>: Python 的第三方插件用来提取 xml 和 HTML 中的数据。 <a href="https://www.crummy.com/software/BeautifulSoup/">官网</a></p><p>1、安装Beautiful Soup</p><p>打开 cmd（命令提示符），进入到 Python（Python2.7版本）安装目录中的 scripts 下，输入 dir 查看是否有 pip.exe, 如果用就可以使用 Python 自带的 pip 命令进行安装，输入以下命令进行安装即可：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install beautifulsoup4</span><br></pre></td></tr></table></figure><p>2、测试是否安装成功</p><p>编写一个 Python 文件，输入:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">import bs4</span><br><span class="line">print bs4</span><br><span class="line"># 运行该文件，如果能够正常输出则安装成功。</span><br></pre></td></tr></table></figure><hr><h3><span id="二-分析网页"><strong>二、分析网页</strong></span></h3><p>实战爬取 <a href="http://www.imomoe.in/top/hottv.html">樱花动漫</a> Top100动漫的下载链接</p><p>（注意：仅供学习思路，如学习请爬取其他网站，尊重他人的服务器，也请自重）</p><h4><span id="0-首先进入主页"><strong>0、首先进入主页</strong></span></h4><p>进入主页后，可以发现这是一个动漫粒度的列表页，我们查看列表出的网页源代码。** 列表处 - 右键 - 检查**</p><p><img src="/2020/06/image-2.png" alt="Top100列表页"></p><p>Top100列表页</p><h4><span id="1-分析top页面"><strong>1、分析Top页面</strong></span></h4><p>先分析网站层级关系，可以发现在class&#x3D;”topli”的div标签下，有无序列表ul&gt;li来存放每个动漫的入口，使用a 标签的href属性指定了当前动漫详细页面的url 如：domain&#x2F;view&#x2F;1794.html（如下图所示）。使用下面代码来获取这些li标签（ 其中domain为该网站域名 <a href="http://www.imomoe.in/">http://www.imomoe.in/</a>  ）</p><p><code>divs = soup.findAll(name=&quot;div&quot;, attrs=&#123;&quot;class&quot;: &quot;topli&quot;&#125;) topk = divs[0].ul.findAll(name=&quot;li&quot;) # 至此，我们的topk已经获取到了所有的动漫的详情页链接</code></p><p><img src="/2020/06/image-1.png"></p><p>Top页面分析</p><h4><span id="2-分析详情页">2、分析详情页</span></h4><p>我们随便点进去一个动漫，可以发现动漫详情页，继续查看源代码，可以发现，这一层的结构和上一层的几乎一致。</p><p><img src="/2020/06/image-5.png"></p><p>动漫详情页</p><h4><span id="3-分析视频播放页"><strong>3、分析视频播放页</strong></span></h4><p>这一步的目标，是通过<video>等标签来分析出视频的下载链接，随便点开一集，检查源代码，可以发现视频是通过一个iframe标签修饰的，而且标签内正好包含下载链接。</video></p><p>其中Iframe标签的src属性中包含着下载链接：<strong><a href="https://saas.jialingmm.net/code.php?type=flv&amp;vid=https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/12846619%5C_6ba8b4e916d2137e040897949fd54e3b.mp4&amp;userlink=http://www.imomoe.in/player/7542-0-0.html&amp;adress=Shanghai">https://saas.jialingmm.net/code.php?type=flv&amp;vid=https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/12846619\_6ba8b4e916d2137e040897949fd54e3b.mp4&amp;userlink=http%3A%2F%2Fwww.imomoe.in%2Fplayer%2F7542-0-0.html&amp;adress=Shanghai</a></strong></p><p><img src="/2020/07/image.png"></p><p>视频播放页</p><hr><h4><span id="4-尝试爬取">4、尝试爬取</span></h4><p>我们通过CMD的curl命令来查看视频播放页的iframe标签（如下图），却发现其中的src为空，并没有像浏览器访问时候所显示的URL，那么，我们如何爬取下载链接呢？</p><p><img src="/2020/07/image-1.png"></p><p>进一步分析，这个iframe标签的src，既然不是访问的时候存在的，则是通过js动态生成的，顺着这个思路，我们寻找一个比较像的js文件，其实就在iframe标签后面（如下图）</p><p><img src="/2020/07/image-2.png"></p><p>我们通过主机域名+js的src，访问，可以发现这个js文件包含这个动漫的所有下载链接，如下：</p><p><img src="/2020/07/image-3.png"></p><p>JS文件</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">var VideoListJson = \[\[&#x27;优酷&#x27;, \[&#x27;\\u7B2C01\\u96C6$https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/12846619\_6ba8b4e916d2137e040897949fd54e3b.mp4$flv&#x27;, &#x27;\\u7B2C02\\u96C6$https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/12846619\_26f1a781b59cddedda710b379d519a45.mp4$flv&#x27;, &#x27;\\u7B2C03\\u96C6$https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/12846619\_14a61ab058bb8baa34fe10afab0f6d9d.mp4$flv&#x27;, &#x27;\\u7B2C04\\u96C6$https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/25330672\_fe7f827fc305a64ffad425635f0a85dc.mp4$flv&#x27;, &#x27;\\u7B2C05\\u96C6$https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/25330672\_45fdd31b21780f648f99169fef713bf5.mp4$flv&#x27;, &#x27;\\u7B2C06\\u96C6$https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/25330672\_d2b1aeb2de9fd044cf797f163f726073.mp4$flv&#x27;, &#x27;\\u7B2C07\\u96C6$https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/25330672\_53044ba7dd316163808c4c54f8b62bce.mp4$flv&#x27;, &#x27;\\u7B2C08\\u96C6$https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/25330672\_c69961fd5359c8827d0695d89e64a1f3.mp4$flv&#x27;, &#x27;\\u7B2C09\\u96C6$https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/25330672\_eccb48c876b6b7f0b26e58c08b1d7d6e.mp4$flv&#x27;, &#x27;\\u7B2C10\\u96C6$https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/607272\_f961e61c07e48e0bcb834e5577837990.mp4$flv&#x27;, &#x27;\\u7B2C11\\u96C6$https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/607272\_7336ab3e3ae49ce9eae3af5d33d64572.mp4$flv&#x27;, &#x27;\\u7B2C12\\u96C6$https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/607272\_3b11ef60d8ec1cd02fa2f2f2f8e08b95.mp4$flv&#x27;, &#x27;\\u7B2C13\\u96C6$https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/607272\_4fcf619108604f6d9e01687e5c69a2bd.mp4$flv&#x27;\]\], \[&#x27;优酷&#x27;, \[&#x27;\\u7B2C01\\u96C6$https://ck-qq.com/v/isgc9ZhZ$zw&#x27;, &#x27;\\u7B2C02\\u96C6$https://ck-qq.com/v/OmVVtwys$zw&#x27;, &#x27;\\u7B2C03\\u96C6$https://ck-qq.com/v/5ufXonER$zw&#x27;, &#x27;\\u7B2C04\\u96C6$https://ck-qq.com/v/a4csbpC5$zw&#x27;, &#x27;\\u7B2C05\\u96C6$https://ck-qq.com/v/3tuLdesn$zw&#x27;, &#x27;\\u7B2C06\\u96C6$https://ck-qq.com/v/GvY6JTTf$zw&#x27;, &#x27;\\u7B2C07\\u96C6$https://ck-qq.com/v/JsTQI54S$zw&#x27;, &#x27;\\u7B2C08\\u96C6$https://ck-qq.com/v/D8eKu51h$zw&#x27;, &#x27;\\u7B2C09\\u96C6$https://ck-qq.com/v/ngr3oW92$zw&#x27;, &#x27;\\u7B2C10\\u96C6$https://ck-qq.com/v/mW79A1RH$zw&#x27;, &#x27;\\u7B2C11\\u96C6$https://ck-qq.com/v/OCzFYdfx$zw&#x27;, &#x27;\\u7B2C12\\u96C6$https://ck-qq.com/v/vyfKm4Hv$zw&#x27;, &#x27;\\u7B2C13\\u96C6$https://ck-qq.com/v/xPhMxCiV$zw&#x27;\]\]\],  </span><br><span class="line">urlinfo = &#x27;http://&#x27; + document.domain + &#x27;/player/7542-&#x27;</span><br></pre></td></tr></table></figure><p>对于上述JS中包含下载链接，我们可以通过正则去分割出下载链接</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">video_urls = list(filter(lambda s: s[:4] == &#x27;http&#x27;, js_text.split(r&#x27;$&#x27;)))</span><br><span class="line"># 其中js_text为上面的js文件内容</span><br></pre></td></tr></table></figure><p><img src="/2020/07/image-4.png"></p><p>运行结果</p><p>最后我们再将链接存到数据库即可。</p><p><img src="/2020/07/image-5.png"></p><p>数据库数据</p><h3><span id="三-附件">三、附件</span></h3><p>源代码Py文件： <a href="https://wws.lanzoux.com/islZ3e7bzfc">蓝奏云下载</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;什么是爬虫？为什么要使用爬虫？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;爬虫（即 网络爬虫 ）是一种按照一定的规则，自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。 （摘取自百度百科）  &lt;/</summary>
      
    
    
    
    <category term="Python" scheme="https://blog.lebear.top/categories/Python/"/>
    
    <category term="python" scheme="https://blog.lebear.top/categories/python/"/>
    
    <category term="数据库操作" scheme="https://blog.lebear.top/categories/python/%E6%95%B0%E6%8D%AE%E5%BA%93%E6%93%8D%E4%BD%9C/"/>
    
    <category term="爬虫" scheme="https://blog.lebear.top/categories/python/%E6%95%B0%E6%8D%AE%E5%BA%93%E6%93%8D%E4%BD%9C/%E7%88%AC%E8%99%AB/"/>
    
    
    <category term="BeautifulSoup" scheme="https://blog.lebear.top/tags/BeautifulSoup/"/>
    
    <category term="bs4" scheme="https://blog.lebear.top/tags/bs4/"/>
    
    <category term="Python" scheme="https://blog.lebear.top/tags/Python/"/>
    
    <category term="数据库操作" scheme="https://blog.lebear.top/tags/%E6%95%B0%E6%8D%AE%E5%BA%93%E6%93%8D%E4%BD%9C/"/>
    
    <category term="网络爬虫" scheme="https://blog.lebear.top/tags/%E7%BD%91%E7%BB%9C%E7%88%AC%E8%99%AB/"/>
    
  </entry>
  
  <entry>
    <title>Arch Linux问题处理</title>
    <link href="https://blog.lebear.top/2020/04/02/162/"/>
    <id>https://blog.lebear.top/2020/04/02/162/</id>
    <published>2020-04-01T20:41:35.000Z</published>
    <updated>2023-04-09T08:28:53.281Z</updated>
    
    <content type="html"><![CDATA[<ul><li>目录</li></ul><!-- toc --><ul><li><a href="#1-%E7%BB%88%E7%AB%AF%E5%85%89%E6%A0%87%E9%94%99%E4%BD%8D">1、终端光标错位</a></li><li><a href="#2-%E5%AE%89%E8%A3%85tim">2、安装Tim</a></li><li><a href="#3-%E5%90%8C%E6%AD%A5%E6%97%B6%E9%97%B4%E9%97%AE%E9%A2%98">3、同步时间问题</a></li><li><a href="#4-%E6%97%A0%E6%B3%95%E6%8C%82%E8%BD%BDwindows%E5%88%86%E5%8C%BA">4、无法挂载windows分区</a></li><li><a href="#5-%E6%9F%A5%E7%9C%8B%E5%86%85%E6%A0%B8%E6%97%A5%E5%BF%97">5、查看内核日志</a></li><li><a href="#6-%E5%85%B3%E6%9C%BA%E9%87%8D%E5%90%AF%E5%8D%A1%E4%BD%8F%E9%97%AE%E9%A2%98">6、关机重启卡住问题</a></li><li><a href="#7-pacman%E6%9B%B4%E6%96%B0%E7%B3%BB%E7%BB%9F%E6%88%96%E5%AE%89%E8%A3%85%E8%BD%AF%E4%BB%B6%E9%94%81%E5%AE%9A">7、pacman更新系统或安装软件锁定</a></li><li><a href="#8-%E5%BC%80%E6%9C%BA%E7%99%BB%E5%BD%95%E7%95%8C%E9%9D%A2%E9%BB%91%E5%B1%8F%E4%BD%86%E6%98%AF%E5%8F%AF%E4%BB%A5%E7%9B%B2%E6%89%93%E5%AF%86%E7%A0%81">8、开机登录界面黑屏，但是可以盲打密码</a></li><li><a href="#9-%E5%8A%A0%E5%85%A5multilib32%E4%BD%8D%E5%BA%93%E6%94%AF%E6%8C%81">9、加入multilib32位库支持</a></li><li><a href="#10-%E6%9B%B4%E6%94%B9%E9%BB%98%E8%AE%A4shell%E8%A7%81-archwiki">10、更改默认shell(见 archwiki)</a></li><li><a href="#11-%E5%BE%85%E8%A1%A5%E5%85%85">11、待补充</a></li><li><a href="#12-%E5%AE%89%E8%A3%85networkmanager%E5%90%8E%E6%B2%A1%E6%B3%95%E4%BD%BF%E7%94%A8%E6%B2%A1%E6%9C%89%E6%89%98%E7%9B%98%E5%9B%BE%E6%A0%87">12、安装NetworkManager后没法使用，没有托盘图标</a></li><li><a href="#13-%E6%B7%BB%E5%8A%A0%E4%B8%AD%E5%9B%BD%E7%A4%BE%E5%8C%BA%E6%BA%90%E6%B8%85%E5%8D%8E%E6%88%96%E8%80%85163%E9%83%BD%E5%8F%AF%E4%BB%A51-%E7%BB%88%E7%AB%AF%E5%85%89%E6%A0%87%E9%94%99%E4%BD%8D">13、添加中国社区源（清华或者163都可以）1、终端光标错位</a></li><li><a href="#14-%E5%AE%89%E8%A3%85%E5%9B%BE%E5%BD%A2%E7%95%8C%E9%9D%A2">14、安装图形界面</a></li><li><a href="#15-nvidia%E6%98%BE%E5%8D%A1%E9%A9%B1%E5%8A%A8%E4%BB%A5%E5%8F%8A%E5%A4%A7%E9%BB%84%E8%9C%82bumblebee">15、nvidia显卡驱动以及大黄蜂bumblebee</a></li><li><a href="#16-make%E7%BC%96%E8%AF%91%E8%BD%AF%E4%BB%B6%E6%97%B6%E4%B8%8D%E8%83%BD%E6%89%A7%E8%A1%8C%E5%8F%AF%E8%83%BD%E5%8E%9F%E5%9B%A0">16、make编译软件时不能执行(可能原因)</a></li><li><a href="#17-bbswitch%E5%8A%A8%E6%80%81%E5%88%87%E6%8D%A2%E4%B8%8D%E8%83%BD%E7%94%A8%E9%9C%80%E8%A6%81%E4%B8%8B%E8%BD%BD%E7%BC%96%E8%AF%91">17、bbswitch动态切换不能用（需要下载编译）</a></li><li><a href="#18-wine%E8%AE%BE%E7%BD%AE%E4%B8%AD%E5%AD%97%E4%BD%93%E5%8F%91%E8%99%9A">18、Wine设置中字体发虚</a></li><li><a href="#19-%E9%85%8D%E7%BD%AEssh%E5%B1%80%E5%9F%9F%E7%BD%91%E9%80%9A%E8%BF%87putty%E7%AD%89ssh%E5%B7%A5%E5%85%B7%E5%AE%89%E8%A3%85%E7%B3%BB%E7%BB%9F">19、配置ssh，局域网通过putty等ssh工具安装系统</a></li><li><a href="#20-%E7%94%BB%E9%9D%A2%E6%92%95%E8%A3%82%E9%97%AE%E9%A2%98%E5%A6%82bilibili">20、画面撕裂问题（如bilibili）</a></li><li><a href="#21-%E8%BE%93%E5%85%A5%E6%B3%95%E5%AE%89%E8%A3%85">21、输入法安装</a></li></ul><!-- tocstop --><h4><span id="1-终端光标错位">1、终端光标错位</span></h4><p><em><strong>修改konsole中的字体为dejaVv即可</strong></em></p><h4><span id="2-安装tim">2、安装Tim</span></h4><p> <em><strong>配置国内社区源&#x2F;etc&#x2F;pacman.conf（见<a href="#13">目录13</a>）</strong></em></p><p> <em><strong>添加32位库支持(见<a href="#09">目录9</a>)</strong></em></p><p><em><strong>然后</strong></em> <em><strong>pacman -Syyu</strong>__<strong>更新软件源</strong></em></p><p><em><strong>之后安装</strong><strong><strong>tim</strong></strong><strong>：</strong>__<strong>sudo pacman -S deepin.com.qq.office</strong></em></p><p><em><strong>最后加一条开机自启动命令</strong></em> <em><strong># &#x2F;usr&#x2F;lib&#x2F;gsd-xsettings</strong></em></p><h4><span id="3-同步时间问题">3、同步时间问题</span></h4><p><em><strong>设置时区：</strong>__<strong>sudo ln -sf &#x2F;usr&#x2F;share&#x2F;zoneinfo&#x2F;Asia&#x2F;Shanghai &#x2F;etc&#x2F;localtime</strong></em></p><p><em><strong>安装</strong><strong><strong>openNTPD</strong></strong><strong>：</strong>__<strong>sudo pacman -S openntpd</strong></em></p><p><em><strong>重启</strong><strong><strong>openNTPD</strong></strong><strong>：</strong>__<strong>systemctl restart openntpd</strong></em></p><p><em><strong>设置开机启动：</strong>__<strong>systemctl enable openntpd</strong></em></p><h4><span id="4-无法挂载windows分区">4、无法挂载windows分区</span></h4><p><em><strong>安装</strong><strong><strong>ntfs</strong></strong><strong>驱动</strong></em> <em><strong>ntfs-3g</strong></em> <em><strong>软件包</strong></em></p><p><em><strong>sudo pacman -S ntfs-3g</strong></em></p><h4><span id="5-查看内核日志">5、查看内核日志</span></h4><p><em><strong>journalctl –dmesg</strong></em></p><h4><span id="6-关机重启卡住问题">6、关机重启卡住问题</span></h4><p><strong>grub编辑中“linux“后面加入acpi_<em>osi&#x3D;”!Windows 2015”</em></strong></p><h4><span id="7-pacman更新系统或安装软件锁定">7、pacman更新系统或安装软件锁定</span></h4><p><em><strong>$pacman -Syu</strong>__<strong>，遇到下列问题：</strong></em></p><p> <em><strong>error: failed to init transaction (unable to lock database)error: could not lock database:…</strong></em></p><p><strong>解决方法：</strong></p><p><em><strong>、</strong>__<strong>$ sudo rm &#x2F;var&#x2F;lib&#x2F;pacman&#x2F;db.lck</strong></em></p><h4><span id="8-开机登录界面黑屏但是可以盲打密码">8、开机登录界面黑屏，但是可以盲打密码</span></h4><p>禁止nvidia驱动（一般安装图形界面完美就不会出现这个问题）</p><p>在 &#x2F;etc&#x2F;modprobe.d 中添加 no-nvidia.conf 文件</p><p>blacklist nvidia</p><p>blacklist nvidia_drm</p><p>blacklist nouveau</p><h4><span id="9-加入multilib32位库支持">9、加入multilib32位库支持</span></h4><p>&#x2F;etc&#x2F;pacman.conf中增加</p><p><strong>[multilib]</strong></p><p><strong>Include &#x3D; &#x2F;etc&#x2F;pacman.d&#x2F;mirrorlist</strong></p><p>通过pacman安装lib32-glibc库就能提供基本的32位支持</p><h4><span id="10-更改默认shell见-archwiki">10、更改默认shell(见 archwiki)</span></h4><p><strong>sudo chsh -s (shell的位置)</strong></p><p>如：sudo chsh -s &#x2F;bin&#x2F;bash</p><h4><span id="11-待补充">11、待补充</span></h4><p><em><strong>空</strong></em></p><h4><span id="12-安装networkmanager后没法使用没有托盘图标">12、安装NetworkManager后没法使用，没有托盘图标</span></h4><p>确定自己安装了plasma-nm</p><p>1 #pacman -S networkmanager plasma-nm</p><p>2 #systemctl start NetworkManager</p><p>3 #systemctl enable NetworkManager</p><h4><span id="13-添加中国社区源清华或者163都可以1-终端光标错位">13、添加中国社区源（清华或者163都可以）1、终端光标错位</span></h4><p>使用方法：在 &#x2F;etc&#x2F;pacman.conf 文件末尾添加以下两行：</p><p>[archlinuxcn]</p><p>Server &#x3D; <a href="https://mirrors.tuna.tsinghua.edu.cn/archlinuxcn/$arch">https://mirrors.tuna.tsinghua.edu.cn/archlinuxcn/$arch</a></p><p>之后安装 archlinuxcn-keyring 包导入 GPG key</p><h4><span id="14-安装图形界面">14、安装图形界面</span></h4><p><strong>安装窗口管理器</strong>pacman -S xorg xorg-xinit</p><p><strong>安装显示管理器</strong>pacman -S sddm sddm-kcm</p><p>设置开机自启systemctl enable sddm</p><p><strong>安装桌面环境</strong>（<a href="https://wiki.archlinux.org/index.php/KDE_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)">参考Wiki</a>）</p><p>最全安装pacman -S plasma</p><p>安装软件pacman -S kde-applications</p><h4><span id="15-nvidia显卡驱动以及大黄蜂bumblebee">15、nvidia显卡驱动以及大黄蜂bumblebee</span></h4><p>见后续文章 nvidia &amp; bumblebee （未更新）</p><h4><span id="16-make编译软件时不能执行可能原因">16、make编译软件时不能执行(可能原因)</span></h4><p>需要把对应uname -a查看的内核版本的kernel header下载</p><p>yaourt linux-header 查看有什么内核头</p><p>（我这里用的是yaourt linux-lts-headers，根据情况）</p><p>最后实际安装的是**core&#x2F;**<strong>linux-lts-headers</strong> <strong>4.19.76-1</strong></p><p>之后就可以make来编译了</p><h4><span id="17-bbswitch动态切换不能用需要下载编译">17、bbswitch动态切换不能用（需要下载编译）</span></h4><p>详细内容见：<a href="https://github.com/Bumblebee-Project/bbswitch">Github link</a></p><p>$ make</p><p>$ sudo make load</p><p>看状态：$cat &#x2F;proc&#x2F;acpi&#x2F;bbswitch </p><p>打开关闭：</p><p>$ sudo tee &#x2F;proc&#x2F;acpi&#x2F;bbswitch &lt;&lt;&lt;OFF</p><p>$ sudo tee &#x2F;proc&#x2F;acpi&#x2F;bbswitch &lt;&lt;&lt;ON</p><p>查看是否成功</p><p>$ dmesg tail -1</p><p>没卸载驱动一般都不可以直接关掉</p><h4><span id="18-wine设置中字体发虚">18、Wine设置中字体发虚</span></h4><p>见后续文章 Wine问题总结（未更新）</p><h4><span id="19-配置ssh局域网通过putty等ssh工具安装系统">19、配置ssh，局域网通过putty等ssh工具安装系统</span></h4><p>如果我们是按照wiki的步骤来装系统，那么一些长而无用的命令无法copy，这时候我们可以尝试局域网其他终端来远程安装。</p><p>用镜像开机后，终端输入</p><p>$ systemctl start sshd  #打开ssh服务</p><p>$ passwd  #设置临时root密码</p><p>然后使用root和设置的password即可ssh连接</p><p>注：systemctl enable sshd可以开机启动ssh服务</p><h4><span id="20-画面撕裂问题如bilibili">20、画面撕裂问题（如bilibili）</span></h4><p><em><strong>浏览网页，看BILIBILI视频的弹幕，如果驱动不是很合适，会出现严重撕裂问题，以下是我的解决方案：</strong></em></p><p>注意：此方法可能会使一些OpenGL程序性能下降，可能会导致WebGL出现一些问题</p><p>（要想更加流畅，Chrome还可以安装一个插件 SmoothScroll:平滑滚动）</p><p>临时命令</p><p>$ nvidia-settings –assign CurrentMetaMode&#x3D;”nvidia-auto-select +0+0 { ForceFullCompositionPipeline &#x3D; On }”</p><p>永久更改方法</p><p>~$ nvidia-xconfig</p><p>~$ cd &#x2F;etc&#x2F;X11&#x2F;</p><p>~$ sudo mv xorg.conf xorg.conf.d&#x2F;20-nvidia.conf</p><p>~$ sudo nano 20-nvidia.conf</p><p>将此文件的 Section “Screen” 部分加入下面几行，其他 Section 可以删掉，然后重启就可以了。</p><p>Section “Screen”</p><p>Identifier “Screen0”</p><p>Option “metamodes” “nvidia-auto-select +0+0 { ForceFullCompositionPipeline &#x3D; On }”</p><p>Option “AllowIndirectGLXProtocol” “off”</p><p>Option “TripleBuffer” “on”</p><p>EndSection</p><h4><span id="21-输入法安装">21、输入法安装</span></h4><p>详细见Wiki，建议安装google输入法，而不是sogou</p><hr>]]></content>
    
    
      
      
    <summary type="html">&lt;ul&gt;
&lt;li&gt;目录&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- toc --&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#1-%E7%BB%88%E7%AB%AF%E5%85%89%E6%A0%87%E9%94%99%E4%BD%8D&quot;&gt;1、终端光标错位&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a h</summary>
      
    
    
    
    <category term="Linux" scheme="https://blog.lebear.top/categories/Linux/"/>
    
    
    <category term="Arch" scheme="https://blog.lebear.top/tags/Arch/"/>
    
    <category term="Linux" scheme="https://blog.lebear.top/tags/Linux/"/>
    
    <category term="指南" scheme="https://blog.lebear.top/tags/%E6%8C%87%E5%8D%97/"/>
    
    <category term="教程" scheme="https://blog.lebear.top/tags/%E6%95%99%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>Spring MVC源码分析（一）：处理前端请求doDispatch()以及getHandler()方法细节</title>
    <link href="https://blog.lebear.top/2020/03/14/142/"/>
    <id>https://blog.lebear.top/2020/03/14/142/</id>
    <published>2020-03-13T20:59:59.000Z</published>
    <updated>2023-04-09T08:28:53.282Z</updated>
    
    <content type="html"><![CDATA[<p>首先： 容器启动时候，会在DispatcherServlet的handlerMappings中添加beanName与类对应的map映射， 以便于后续getHandler时候用{GET &#x2F;test&#x2F;t1}来取得对应请求的方法 。</p><p><img src="https://zhangxujie.tk/wp-content/uploads/2020/03/image-10.png"></p><p>map映射</p><p>简要流程：</p><hr><ol><li>所有请求发来，到DispatcherServlet收到请求</li><li>调用doDispatch()方法进行处理<ol><li>getHandler()：根据请求地址和mappingRegistry（上面有提到，可查找）找到能处理这个请求的目标处理类（处理器&lt;控制器&gt;）</li><li>getHandlerAdapter()：根据当前处理器类（控制器）获取能处理这个处理器方法的适配器；</li><li>使用获得的适配器ha(AnnotationMethodHandlerAdapter)执行目标方法</li><li>适配器调用目标方法后，都会返回ModelAndView对象<strong>mv &#x3D; ha.handle(processedRequest</strong><strong>,</strong> <strong>response</strong><strong>,</strong> <strong>mappedHandler.getHandler())</strong></li><li>根据ModelAndView信息转发到具体页面，并可以在请求域中取得对象的Model数据</li></ol></li></ol><hr><p>前端发送请求后，在DispatcherServlet类中， 底层调用doDispatch()方法 。</p><p><code>protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception &#123; HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try &#123; ModelAndView mv = null; Exception dispatchException = null; try &#123; //1、检查是否为文件上传请求 processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. //2、决定哪个控制器处理这个请求，下面有代码解释 mappedHandler = getHandler(processedRequest); //取出后会发现值为：public java.lang.String com.zhangxujie.controller.TestController.t1() //3、如果未找到处理这个请求的控制器，则异常等处理 if (mappedHandler == null) &#123; noHandlerFound(processedRequest, response); return; &#125; // Determine handler adapter for the current request. //4、知道请求的类后，要拿到响应的适配器去处理请求，而不是用原始类处理（拿到反射工具） HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); //取得了对应的注解适配器，目的可能为了方便Spring的AOP操作 // Process last-modified header, if supported by the handler. // 获取请求方式，当前为GET String method = request.getMethod(); boolean isGet = &quot;GET&quot;.equals(method); if (isGet &quot;HEAD&quot;.equals(method)) &#123;//如果是GET请求，如何如何，加缓存等等 long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) &amp;&amp; isGet) &#123; return; &#125; &#125; if (!mappedHandler.applyPreHandle(processedRequest, response)) &#123; return; &#125; //5、适配器执行目标方法 // Actually invoke the handler，处理器方法被真正调用 //控制器（Controller）=处理器（handler） mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); //通过适配器执行目标方法，返回ModelAndView对象（本段代码结束后图一） //代码内部在7、Spring MVC 部分源码分析(2) //无论目标方法如何写，最终适配器都要执行后返回ModelAndView对象 if (asyncManager.isConcurrentHandlingStarted()) &#123; return; &#125; applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); &#125; catch (Exception ex) &#123; dispatchException = ex; &#125; catch (Throwable err) &#123; // As of 4.3, we&#39;re processing Errors thrown from handler methods as well, // making them available for @ExceptionHandler methods and other scenarios. dispatchException = new NestedServletException(&quot;Handler dispatch failed&quot;, err); &#125; //6、根据方法最终执行后封装的ModelAndView转发到对应页面，而且ModelAndView中的数据可以从请求域中获取 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); &#125; catch (Exception ex) &#123; triggerAfterCompletion(processedRequest, response, mappedHandler, ex); &#125; catch (Throwable err) &#123; triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException(&quot;Handler processing failed&quot;, err)); &#125; finally &#123; if (asyncManager.isConcurrentHandlingStarted()) &#123; // Instead of postHandle and afterCompletion if (mappedHandler != null) &#123; mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); &#125; &#125; else &#123; // Clean up any resources used by a multipart request. if (multipartRequestParsed) &#123; cleanupMultipart(processedRequest); &#125; &#125; &#125; &#125;</code></p><p>其中，方法开始时调用getHandler()方法，是用来确定哪个控制器来处理这个请求，同时获得目标处理器执行链（包括拦截器）</p><p><strong>getHandler细节</strong></p><p><code>protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception &#123; if (this.handlerMappings != null) &#123; for (HandlerMapping mapping : this.handlerMappings) &#123;//遍历Handlermap，以取得对应控制器 //此处进入下面的方法getHandlerInternal() HandlerExecutionChain handler = mapping.getHandler(request); //执行到这里，已经获得了执行控制器TestController.t1() if (handler != null) &#123; return handler; &#125; &#125; &#125; return null; &#125; ///////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////// /** * Look up a handler method for the given request. */ @Override protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception &#123; String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); //mappingRegistry存着所有控制器请求url和对应类方法，（本段代码结束后图一），下面要查找bean，所以上读锁 this.mappingRegistry.acquireReadLock(); try &#123; //通过请求url找到最符合的请求方法，当前为TestController.t1() HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); &#125; finally &#123; this.mappingRegistry.releaseReadLock(); &#125; &#125;</code></p><p><img src="https://zhangxujie.tk/wp-content/uploads/2020/03/image-11.png"></p><p>图一</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;首先： 容器启动时候，会在DispatcherServlet的handlerMappings中添加beanName与类对应的map映射， 以便于后续getHandler时候用{GET &amp;#x2F;test&amp;#x2F;t1}来取得对应请求的方法 。&lt;/p&gt;
&lt;p&gt;&lt;img s</summary>
      
    
    
    
    <category term="框架学习" scheme="https://blog.lebear.top/categories/%E6%A1%86%E6%9E%B6%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="java" scheme="https://blog.lebear.top/tags/java/"/>
    
    <category term="Spring MVC" scheme="https://blog.lebear.top/tags/Spring-MVC/"/>
    
    <category term="源码分析" scheme="https://blog.lebear.top/tags/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
    
  </entry>
  
  <entry>
    <title>Linux:添加开关机蜂鸣器提示</title>
    <link href="https://blog.lebear.top/2020/03/11/136/"/>
    <id>https://blog.lebear.top/2020/03/11/136/</id>
    <published>2020-03-10T16:00:00.000Z</published>
    <updated>2023-04-09T08:28:53.281Z</updated>
    
    <content type="html"><![CDATA[<p>应用：Linux服务器在没有音频输出和显示输出的情况下，利用蜂鸣器可以快速判断启动状况。</p><p>添加systemd服务：</p><p># &#x2F;etc&#x2F;systemd&#x2F;system&#x2F;beep-up.service</p><p>[Unit]<br>Description&#x3D;Beep after system start<br>DefaultDependencies&#x3D;no<br>After&#x3D;multi-user.target</p><p>[Service]<br>Type&#x3D;oneshot<br>ExecStart&#x3D;&#x2F;usr&#x2F;bin&#x2F;beep -f 3000 -l 100 -n -f 3500 -l 100 -r 2</p><p>[Install]<br>WantedBy&#x3D;multi-user.target</p><p># &#x2F;etc&#x2F;systemd&#x2F;system&#x2F;beep-down.service</p><p>[Unit]<br>Description&#x3D;Beep before system shutdown<br>DefaultDependencies&#x3D;no<br>Before&#x3D;exit.target</p><p>[Service]<br>Type&#x3D;oneshot<br>ExecStart&#x3D;&#x2F;usr&#x2F;bin&#x2F;beep -f 3000 -l 100 -r 2 -n -f 2000 -l 150</p><p>[Install]<br>WantedBy&#x3D;reboot.target halt.target poweroff.target</p><p>最后启用 beep-up.service 和 beep-down.service</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;应用：Linux服务器在没有音频输出和显示输出的情况下，利用蜂鸣器可以快速判断启动状况。&lt;/p&gt;
&lt;p&gt;添加systemd服务：&lt;/p&gt;
&lt;p&gt;# &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;beep-up.service&lt;/p&gt;
&lt;p&gt;</summary>
      
    
    
    
    <category term="Linux" scheme="https://blog.lebear.top/categories/Linux/"/>
    
    
    <category term="Linux" scheme="https://blog.lebear.top/tags/Linux/"/>
    
  </entry>
  
  <entry>
    <title>剑指offer：字符串的排列（划分子问题）</title>
    <link href="https://blog.lebear.top/2020/02/08/110/"/>
    <id>https://blog.lebear.top/2020/02/08/110/</id>
    <published>2020-02-08T00:53:20.000Z</published>
    <updated>2023-04-09T08:28:53.282Z</updated>
    
    <content type="html"><![CDATA[<h3><span id="问题描述">问题描述：</span></h3><p>求解一个字符串中字符的全排列。</p><p>如：给定字符串”<strong>abc</strong>“</p><p>求出全排列： <strong>abc, acb, bac, bca, cad, cba</strong></p><h3><span id="思路"><strong>思路：</strong></span></h3><p>这道题排列不是问题，问题是方法不对会导致结果有重复元素。如果不能够一次性不重复排列，则会在去重上大费功夫。</p><p>这个问题我们不难想到，与“青蛙跳台阶”、“斐波那契数列”等问题解决方法有异曲同工之妙，仔细看来，这个问题也是可以划分为子问题，解决子问题进而逐步解决整体。但是，什么是子问题呢？</p><p>举个例子，我们用输入字符串 “<strong>abcd</strong>“ 来说明：</p><p>（1）我们要求<strong>abcd</strong>的全排列，就可以分别以 <strong>a, b, c, d</strong> 开头，然后求 <strong>bcd, acd, bad, bca</strong> 的全排列。</p><p>（2）循环求上述四种情况，对于其中之一 <strong>a</strong>开头，后接<strong>bcd</strong>的情况，又可以对子串”<strong>bcd</strong>“进行（<strong>1</strong>）操作——即求<strong>bcd</strong>全排列。</p><p>（3）直到子问题级别到达只剩一个子串的全排列时候，就可以递归返回上一层了。</p><h3><span id="解决方法-java代码">解决方法 - java代码：</span></h3><p><code>public class 字符串的排列 &#123; public static List permutation(String str)&#123; if(str == null str.length() == 0)&#123;//如果字符串为空，直接返回空 return null; &#125; List list = new ArrayList&lt;&gt;();//实例化一个容器，用来存放全排列结果 permutationCore(str.toCharArray(), 0, list); &#125; public static void permutationCore(char[] chs, int index, List list)&#123; if(index == chs.length)&#123;//上述abcd，当index移动到结尾字符d时候，可以选择把abcd加入list了 list.add(String.valueOf(chs)); &#125; for(int i = index; i &lt; chs.length; i++)&#123; //i从index到结尾下标，可以使每次问题字符串的开头字符遍历完 //第一次：a bcd //第二次：b acd //...... char tmp = chs[i]; chs[i] = chs[index]; chs[index] = tmp; permutationCore(chs, index + 1, list);//解决当前字符开头的子串对应的子问题，如bcd, acd... //子问题解决完毕后，还需要恢复 //如第二次循环前面str变成了bacd //接下来还有再变回abcd，才能继续下一轮该表首字符 tmp = chs[i]; chs[i] = chs[index]; chs[index] = tmp; &#125; &#125; public static void main(String[] args) &#123;//测试程序 String str = &quot;abcd&quot;; List list = permutation(str); list.forEach(c -&gt; System.out.print(c + &quot; - &quot;)); &#125; &#125;</code></p><p>运行结果：</p><p><strong>abcd - abdc - acbd - acdb - adcb - adbc - bacd - badc - bcad - bcda - bdca - bdac - cbad - cbda - cabd - cadb - cdab - cdba - dbca - dbac - dcba - dcab - dacb - dabc -</strong></p><hr>]]></content>
    
    
      
      
    <summary type="html">&lt;h3&gt;&lt;span id=&quot;问题描述&quot;&gt;问题描述：&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;求解一个字符串中字符的全排列。&lt;/p&gt;
&lt;p&gt;如：给定字符串”&lt;strong&gt;abc&lt;/strong&gt;“&lt;/p&gt;
&lt;p&gt;求出全排列： &lt;strong&gt;abc, acb, bac, bca, cad, </summary>
      
    
    
    
    <category term="算法" scheme="https://blog.lebear.top/categories/%E7%AE%97%E6%B3%95/"/>
    
    
    <category term="java" scheme="https://blog.lebear.top/tags/java/"/>
    
    <category term="数据结构" scheme="https://blog.lebear.top/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="算法" scheme="https://blog.lebear.top/tags/%E7%AE%97%E6%B3%95/"/>
    
    <category term="剑指offer" scheme="https://blog.lebear.top/tags/%E5%89%91%E6%8C%87offer/"/>
    
    <category term="字符串" scheme="https://blog.lebear.top/tags/%E5%AD%97%E7%AC%A6%E4%B8%B2/"/>
    
    <category term="递归" scheme="https://blog.lebear.top/tags/%E9%80%92%E5%BD%92/"/>
    
  </entry>
  
  <entry>
    <title>剑指offer：二叉搜索树与双向链表</title>
    <link href="https://blog.lebear.top/2020/02/07/83/"/>
    <id>https://blog.lebear.top/2020/02/07/83/</id>
    <published>2020-02-07T03:39:13.000Z</published>
    <updated>2023-04-09T08:28:53.282Z</updated>
    
    <content type="html"><![CDATA[<h3><span id="问题描述">问题描述：</span></h3><p>输入一棵二叉搜索树，将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点，只能调整树中结点指针的指向，比如输入下图中左边的二叉搜索树，则输出转换之后的排序双向链表。</p><p><img src="/2020/02/image-4.png"></p><p>二叉搜索树</p><p>转化为如下双向链表</p><p><img src="/2020/02/image-5.png"></p><h3><span id="思路"><strong>思路：</strong></span></h3><p>二叉搜索树转化为双向链表，可以看作中序遍历二叉树</p><p>创建一个栈stack用来存储遍历的节点</p><p>然后设置两个TreeNode节点（<strong>lastNode、p</strong>）</p><p>—— lastNode用来存储上一次操作的节点</p><p>—— p用来存储当前操作节点</p><p>while循环判断左子树，遇到的节点都入栈，直到下一个节点为null。</p><p>然后开始出栈，出栈后，把上一个节点(lastNode!&#x3D;null情况下)的right域赋值为当前节点p，然后把当前节点p的left域设置为lastNode。</p><p>p指针右移</p><h4><span id="treenode节点信息">TreeNode节点信息</span></h4><p><code>//TreeNode结构 public class TreeNode &#123; int val; TreeNode left = null; TreeNode right = null; TreeNode parent = null; TreeNode(int val) &#123; this.val = val; &#125; TreeNode() &#123; &#125; public static TreeNode createTreeNode(int val) &#123; TreeNode node = new TreeNode(); node.val = val; return node; &#125; public static void connectTreeNode(TreeNode root, TreeNode left, TreeNode right) &#123; if (root != null) &#123; root.left = left; root.right = right; if (left != null) &#123; left.parent = root; &#125; if (right != null) &#123; right.parent = root; &#125; &#125; &#125; &#125;</code></p><h3><span id="两种解决">两种解决：</span></h3><h4><span id="一-非递归方式">一、非递归方式</span></h4><p><code>public class BinSearchTree2DLinkedList &#123; public static void main(String[] args) &#123; TreeNode t = new TreeNode(); TreeNode pNode8 = t.createTreeNode(8); TreeNode pNode6 = t.createTreeNode(6); TreeNode pNode10 = t.createTreeNode(10); TreeNode pNode5 = t.createTreeNode(5); TreeNode pNode7 = t.createTreeNode(7); TreeNode pNode9 = t.createTreeNode(9); TreeNode pNode11 = t.createTreeNode(11); t.connectTreeNode(pNode8, pNode6, pNode10); t.connectTreeNode(pNode6, pNode5, pNode7); t.connectTreeNode(pNode10, pNode9, null); TreeNode node = transform(pNode8); while (node != null)&#123; System.out.print(node.val + &quot;-&quot;); node = node.right; &#125; &#125; //非递归方式 public static TreeNode transform(TreeNode root)&#123; if (root == null)&#123; return root; &#125; Stack stack = new Stack&lt;&gt;(); TreeNode lastNode = null;//构建双向链表时候，上一个节点，为了下一个节点连接left用 TreeNode p = root; /* * 8 * 6 10 * 5 7 9 * */ while (!stack.empty() p != null)&#123; while (p != null)&#123;//一直找到最左子树，即5，然后到下一个 stack.push(p); p = p.left;// &#125; if (!stack.empty())&#123; p = stack.pop(); if (lastNode != null)&#123; lastNode.right = p; &#125; p.left = lastNode; lastNode = p; p = p.right;//p走到当前节点的右子树， &#125; &#125; p = root; while (p.left != null)&#123; p = p.left; &#125; return p; &#125; &#125;</code></p><p><img src="/2020/02/image-6.png"></p><p>运行结果</p><h4><span id="二-递归方式">二、递归方式</span></h4><p>这个方法就比较简单了，与中序遍历基本相同，大家可以自行思考，有问题请留言。</p><hr><p><strong>其他文章</strong></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h3&gt;&lt;span id=&quot;问题描述&quot;&gt;问题描述：&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;输入一棵二叉搜索树，将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点，只能调整树中结点指针的指向，比如输入下图中左边的二叉搜索树，则输出转换之后的排序双向链表。&lt;/p&gt;
&lt;p&gt;&lt;img</summary>
      
    
    
    
    <category term="算法" scheme="https://blog.lebear.top/categories/%E7%AE%97%E6%B3%95/"/>
    
    
    <category term="java" scheme="https://blog.lebear.top/tags/java/"/>
    
    <category term="二叉搜索树" scheme="https://blog.lebear.top/tags/%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/"/>
    
    <category term="数据结构" scheme="https://blog.lebear.top/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="算法" scheme="https://blog.lebear.top/tags/%E7%AE%97%E6%B3%95/"/>
    
  </entry>
  
  <entry>
    <title>Arch Linux 安装 新手指北</title>
    <link href="https://blog.lebear.top/2020/02/05/21/"/>
    <id>https://blog.lebear.top/2020/02/05/21/</id>
    <published>2020-02-04T19:32:44.000Z</published>
    <updated>2023-04-09T08:28:53.280Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>参考 ArchWiki Installation guide</p><p><a href="https://wiki.archlinux.org/index.php/Installation_guide">https://wiki.archlinux.org/index.php/Installation_guide</a></p></blockquote><h2><span id="一安装前的准备-pre-installation">（一）安装前的准备 <em>Pre-installation</em></span></h2><h3><span id="1下载镜像">（1）下载镜像</span></h3><p>首先<a href="https://archlinux.org/download/">下载页面</a>下载需要的镜像</p><p><img src="/2020/02/image-1.png"></p><p>推荐磁力下载</p><p><img src="/2020/02/image-2.png"></p><p>当然也可以在国内镜像站下载</p><p>至于官方所说验证签名，大家可以验证一下，一般情况下在这些正规网站下载不大会出问题，这里笔者就省略了。</p><hr><h3><span id="2安装介质-boot-the-live-environment">（2）安装介质 <em>Boot the live environment</em></span></h3><p>笔者这里选用的是u盘安装，可以使用 ultralISO或 rufus 来将（1）下载好的镜像写入u盘，如果不会可以google一下，或者留言，笔者可以视情况再 详细介绍。</p><p>打开ISO文件 –&gt; 选择下载好的ISO文件 –&gt; 启动 –&gt; 写入硬盘映像 –&gt; 选中启动u盘（写入方式：raw 需要格式化，请提前备份）–&gt; 写入</p><p><img src="/2020/02/image-3.png"></p><p>UltraISO 写入镜像</p><hr><h2><span id="二开始安装">（二）开始安装</span></h2><p>首先选择u盘启动，进入live，不同的主板方式不同，笔者开机长按F7即可选择启动方式。</p><h3><span id="1硬盘分区">（1）硬盘分区</span></h3><p>用parted对硬盘分区</p><p><code>$ parted /dev/sda</code></p><p>1、先创建分区表 ：</p><p><code>$ mklabel gpt</code></p><p>2、然后创建ESP分区：</p><p><code>$ mkpart ESP fat32 1M 513M</code></p><p>3、然后创建primary主分区：</p><p><code>$ mkpart / ext4 513M 100%</code></p><p>退出gparted</p><p>4、格式化两个分区</p><p><code>$ mkfs.vfat -F32 /dev/sda1</code></p><p><code>$ mkfs.ext4 /dev/sda2</code></p><p>5、挂载分区ESP：</p><p><code>$ mount /dev/sda1 /mnt/efi</code></p><p>   挂载&#x2F;主分区：</p><p><code>$ mount /dev/sda2 /mnt</code></p><h3><span id="2配置安装源">（2）配置安装源</span></h3><p>1、配置国内镜像源：</p><p><code>$ vi /etc/pacman.d/mirrorlist</code></p><p>2、添加如下内容</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Server = https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch</span><br><span class="line">Server = http://mirrors.aliyun.com/archlinux/$repo/os/$arch</span><br><span class="line">Server = http://mirrors.163.com/archlinux/$repo/os/$arch</span><br></pre></td></tr></table></figure><p>3、更新镜像源 ：</p><p><code>$ pacman -Syy</code></p><p>4、安装基本系统：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ pacstrap /mnt base [base-devel] # 后面的包可缺省,最好一起下上</span><br></pre></td></tr></table></figure><p>5、配置系统，生成fstab：</p><p>$ genfstab -U &#x2F;mnt &gt;&gt; &#x2F;mnt&#x2F;etc&#x2F;fstab</p><p><strong>强烈建议</strong> 在执行完以上命令后，后检查一下生成的 &#x2F;mnt&#x2F;etc&#x2F;fstab 文件是否正确</p><p>6、Change root到新系统：</p><p><code>$ arch-chroot /mnt</code></p><p>进入新挂载的系统后</p><p>设置时区：</p><p><code>$ ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime</code></p><p>同步硬件时间：</p><p><code>$ hwclock –systohc</code></p><h3><span id="3本地化设置"><strong>（3）本地化设置</strong></span></h3><p>1 、设置本地化文本配置，在locale.gen 中取消本地化文本的注释，如下三个即可</p><p><code>$ nano /etc/locale.gen</code></p><p><code>en_US.UTF-8 UTF-8 zh_CN.UTF-8 UTF-8 zh_TW.UTF-8 UTF-8</code></p><p>2、由配置生成本地化讯息：</p><p><code>$ locale-gen</code></p><p>3、创建 locale.conf 并编辑 LANG 这一 变量</p><p>$ echo LANG&#x3D;en_US.UTF-8 &gt; &#x2F;etc&#x2F;locale.conf</p><p>4、设置网络</p><p>（a）创建hostname文件(myhostname根据自己情况修改)</p><p>       $ echo myhostname &gt; &#x2F;etc&#x2F;hostname</p><p>（b）在hosts中添加信息</p><p>       <code>$ nano /etc/hosts</code></p><p>       添加如下内容：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">127.0.0.1 localhost   </span><br><span class="line">::1 localhost   </span><br><span class="line">127.0.1.1 myhostname.localdomain myhostname </span><br></pre></td></tr></table></figure><p>5、安装network manager</p><p><code>$ pacman -S networkmanager plasma-nm</code></p><p><code>$ systemctl start NetworkManager</code></p><p><code>$ systemctl enable NetworkManager</code></p><p>6、为root设置密码</p><p><code>$ passwd #然后输入要设置的root密码</code></p><h3><span id="3安装grub">（3）安装grub</span></h3><p>1、先安装启动引导器grub和efibootmgr</p><p>    <code>$ pacman -S grub efibootmgr</code></p><p>2、（如果之前安装了windows 直接用windows的esp就行，不需要执行这一步）挂载EFI分区（一般之前都挂载了(挂载点esp为&#x2F;efi)）</p><p>然后执行下面语句</p><p><code>$ mkdir -p esp/EFI/arch</code></p><p><code>$ cp -a /boot/vmlinuz-linux esp/EFI/arch/</code></p><p><code>$ cp -a /boot/initramfs-linux.img esp/EFI/arch/</code></p><p><code>$ cp -a /boot/initramfs-linux-fallback.img esp/EFI/arch/</code></p><p>将 GRUB EFI 应用 grubx64.efi 安装到 <em>esp</em>&#x2F;EFI&#x2F;GRUB&#x2F;，并将其模块安装到 &#x2F;boot&#x2F;grub&#x2F;x86_64-efi&#x2F;</p><p><code>$ grub-install --target=x86_64-efi --efi-directory=/efi/ --bootloader-id=GRUB</code></p><p>3、生产主配置文件</p><p><code>$ grub-mkconfig -o /boot/grub/grub.cfg</code></p><p>4、然后退出新系统：<strong>ctrl+D</strong>  </p><p>5、递归卸载各个挂载点：</p><p><code>$ umount -R /mnt</code></p><p>6、重启：<strong>reboot</strong></p><p><strong>重启进入如果没网，配置启动dhcp</strong>，命令如下</p><p>       <code>$ systemctl enable dhcpcd.service</code></p><p>      <code>$ systemctl start dhcpcd.service</code></p><h3><span id="4安装图形界面-创建用户">（4）安装图形界面、创建用户</span></h3><p>1、安装显卡驱动</p><p> <code>$ pacman -S xf86-video-intel</code></p><p> <code>$ pacman -S nvidia</code> </p><p>2、安装图形界面，需要安装xorg 窗口管理器（图形界面的基础）</p><p>       $ pacman -S xorg xorg-xinit     # 为了可以使用startx命令进入桌面</p><p>3、安装一个桌面环境，如LXDE、kde等</p><p><code>$ pacman -S lxde</code></p><p>4、安装sddm登陆管理器（窗口管理器）来<strong>代替</strong>手动启动桌面环境命令：</p><p><code>$ startx</code></p><pre><code>      `$ pacman -S sddm # 安装登陆管理器(显示管理器)`      `$ systemctl enable sddm.service # 配置开机启动`</code></pre><p>5、创建用户，由于不能通过root进入图形界面，所以接下来创建用户（名为username）</p><p><code>$ useradd -m -G wheel -s /bin/bash username</code></p><p>并且自动在home中建立该用户的目录</p><p>6、然后修改用户密码：</p><p><code>$ passwd username</code></p><p>7、如果<strong>没有</strong>sudo则安装</p><p> <code>$ pacman -S sudo</code></p><p>8、然后给wheel用户组sudo权限</p><p><code>$ visudo</code></p><p>把这一行 <strong>%wheel ALL&#x3D;(ALL) ALL</strong> 的注释去掉</p><p>9、安装思源黑体</p><p><code>$ pacman -S adobe-source-han-sans-cn-fonts</code></p><p>至此，Arch的基本安装步骤已经完成，之后还会发布有关系统优化和本地化指南，以及笔者在安装使用过程中遇到的问题和解决方案。</p><hr><p><strong>其他文章</strong></p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;参考 ArchWiki Installation guide&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://wiki.archlinux.org/index.php/Installation_guide&quot;&gt;https://wiki.archlin</summary>
      
    
    
    
    <category term="Linux" scheme="https://blog.lebear.top/categories/Linux/"/>
    
    
    <category term="Arch" scheme="https://blog.lebear.top/tags/Arch/"/>
    
    <category term="Linux" scheme="https://blog.lebear.top/tags/Linux/"/>
    
    <category term="OS" scheme="https://blog.lebear.top/tags/OS/"/>
    
    <category term="指南" scheme="https://blog.lebear.top/tags/%E6%8C%87%E5%8D%97/"/>
    
    <category term="操作系统" scheme="https://blog.lebear.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
  </entry>
  
  <entry>
    <title>Linux下网易云音乐打开无反应</title>
    <link href="https://blog.lebear.top/2020/02/04/19/"/>
    <id>https://blog.lebear.top/2020/02/04/19/</id>
    <published>2020-02-04T14:30:41.000Z</published>
    <updated>2023-04-09T08:28:53.281Z</updated>
    
    <content type="html"><![CDATA[<p><strong>解决方法一：</strong>在终端 <em>sudo netease-cloud-music</em> 以root权限运行</p><p><strong>解决方法二：</strong>桌面图标右键以文本方式打开，然后找到Exec&#x3D;这一行 改为以下内容 <em>Exec&#x3D;sudo netease-cloud-music %U –nosandbox</em></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;strong&gt;解决方法一：&lt;/strong&gt;在终端 &lt;em&gt;sudo netease-cloud-music&lt;/em&gt; 以root权限运行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方法二：&lt;/strong&gt;桌面图标右键以文本方式打开，然后找到Exec&amp;#x3D;这一行 改为以下</summary>
      
    
    
    
    <category term="Linux" scheme="https://blog.lebear.top/categories/Linux/"/>
    
    
  </entry>
  
  <entry>
    <title>centos 7 如何添加zookeeper自启动服务</title>
    <link href="https://blog.lebear.top/2020/02/04/16/"/>
    <id>https://blog.lebear.top/2020/02/04/16/</id>
    <published>2020-02-04T14:18:49.000Z</published>
    <updated>2023-04-09T08:28:53.281Z</updated>
    
    <content type="html"><![CDATA[<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ vim zookeeper    # 编辑一个shell脚本,如果没有安装vim可用nano，vi等代替vim</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">内容如下：</span><br><span class="line"></span><br><span class="line">#!/bin/bash  </span><br><span class="line">#chkconfig:2345 20 90  </span><br><span class="line">#description:zookeeper  </span><br><span class="line">#processname:zookeeper  </span><br><span class="line">export JAVA_HOME=/usr/java/default</span><br><span class="line">case $1 in  </span><br><span class="line">        start) su - zookeeper -c &#x27;/usr/local/zookeeper-3.4.5/bin/zkServer.sh start&#x27;;;  </span><br><span class="line">        stop) su - zookeeper -c &#x27;/usr/local/zookeeper-3.4.5/bin/zkServer.sh stop&#x27;;;  </span><br><span class="line">        status) su - zookeeper -c &#x27;/usr/local/zookeeper-3.4.5/bin/zkServer.sh status&#x27;;;  </span><br><span class="line">        restart) su - zookeeper -c &#x27;/usr/local/zookeeper-3.4.5/bin/zkServer.sh restart&#x27;;;  </span><br><span class="line">        *) echo &quot;require startstopstatusrestart&quot; ;;  </span><br><span class="line">esac  </span><br></pre></td></tr></table></figure><p>注意：JAVA_HOME 路径要根据自己的来； zookeeper的路径也要根据自己的来；</p><hr><p>使用方法：</p><p>启动：service zookeeper start</p><p>停止：service zookeeper stop</p><p>查看状态：service zookeeper status</p><p>重启服务：service zookeeper restart</p>]]></content>
    
    
      
      
    <summary type="html">&lt;figure class=&quot;highlight plaintext&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span</summary>
      
    
    
    
    <category term="zookeeper" scheme="https://blog.lebear.top/categories/zookeeper/"/>
    
    
  </entry>
  
  <entry>
    <title>商城项目中踩到的坑(Hadoop、Kafka、Zookeeper、redis、hbase、Mysql)</title>
    <link href="https://blog.lebear.top/2020/02/03/11/"/>
    <id>https://blog.lebear.top/2020/02/03/11/</id>
    <published>2020-02-03T06:20:41.000Z</published>
    <updated>2023-04-09T08:28:53.282Z</updated>
    
    <content type="html"><![CDATA[<p>1、前端出现此错误(背景：用springMVC做的数据库增删改查小项目，使用gradle管理，前后端分离两模块；时间20190821)</p><p><img src="https://s2.ax1x.com/2020/02/04/10xyvR.png"></p><p>问题(前后端连接问题)可能原因：</p><p>--打开的网址和前端请求网址一致性问题，需要把前端请求网址全字段写出来(如<a href="http://localhost:8080(8082服务器端口号)/user/add">http://localhost:8080(8082服务器端口号)/user/add</a>)；</p><p>--还要注意后面方法名是否一致(如&#x2F;queryAll写成了&#x2F;queryall)，注意请求方法前后端一致：get,post,delete,put</p><p>--还有Controller层是否加了@CrossOrigin，目的是为了允许跨域访问</p><p>--contentType:”application&#x2F;json”，如果前端像后端传值为json格式(JSON.stringify($(“#form1”).serializeObject()))，ajax必须设置这一项，不然也会出现类似错误。</p><hr><p>2、数据库连接错误(前端提示为500)，此时需要把配置文件中数据库的密码加上单引号  如<strong>0412</strong>改为<strong>’0412’</strong></p><p><img src="https://s2.ax1x.com/2020/02/04/10xgDx.png"></p><hr><p>3、因为前后端分离，然后访问端口的问题</p><p><img src="https://s2.ax1x.com/2020/02/04/10xs29.png"></p><p>前端还会提示html中的ajax处有错误</p><p>仔细观察，的确，Tomcat开的端口号为8082，在配置文件yml中不但到配置，还要在前端访问的url中改写把url&#x3D;”http:&#x2F;&#x2F; localhost:<strong>8080</strong>&#x2F;user&#x2F;query”改为url&#x3D;”http:&#x2F;&#x2F; localhost:<strong>8082</strong>&#x2F;user&#x2F;query”</p><p>端口号与之一致；还有可能就是provider的程序没运行完，然后consumer就开始运行了</p><hr><p>4、</p><p>Description:（SpringBoot错误）<br>Failed to configure a DataSource: ‘url‘ attribute is not specified and no embedded datasource could be configured.<br>Reason: Failed to determine a suitable driver class<br>Action:<br>Consider the following:<br>If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.<br>If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).</p><p>解决办法:</p><p>程序入口处:在间的包下(如com.zhangxujie.consumer中刚建好就有的程序入口类)<br>@SpringBootApplication<br>public class DemoApplication {<br>修改为:<br>@SpringBootApplication(exclude &#x3D; DataSourceAutoConfiguration.class)<br>public class DemoApplication {</p><p>-—记得在Service层加@Service注解<br>-—看看是否 程序入口的 xxxApplication.java 是否在最外层的包下.如果不是,把该类放在最外层的包下面</p><p>-—可能包没有扫到,继续添加:</p><p>@SpringBootApplication(exclude &#x3D; DataSourceAutoConfiguration.class,scanBasePackages&#x3D;{ “com.zhangxujie.consumer”})<br>public class DemoApplication {</p><hr><p>5、</p><p><img src="https://s2.ax1x.com/2020/02/04/10xfUO.png"></p><p>Zookeeper服务器问题，可能没有开，或者host中映射关系问题</p><hr><p>6、前端顺利向后端传送数据，但是</p><p><img src="https://s2.ax1x.com/2020/02/04/10x2b6.png"></p><p>错误显示出现在ajax</p><p>但有异常显示，会发现，我们的model层User未实现序列化(implement Serializable)</p><hr><p>7、机器学习启动时候，出现socket error，检查hbase配置中2181端口号是否正确</p><hr><p>8、异常处理</p><p>2019-11-08 19:51:46.468 ERROR 6372 — [nio-8082-exec-1]</p><p>o.a.c.c.C.[.[.[&#x2F;].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is com.alibaba.dubbo.rpc.RpcException: Forbid consumer 192.168.56.1 access service tk.zhangxujie.api.service.IMyService from registry 192.168.56.3:2181 use dubbo version 2.5.3, Please check registry access list (whitelist&#x2F;blacklist).] with root cause</p><p>com.alibaba.dubbo.rpc.RpcException: Forbid consumer 192.168.56.1 access service tk.zhangxujie.api.service.IMyService from registry 192.168.56.3:2181 use dubbo version 2.5.3, Please check registry access list (whitelist&#x2F;blacklist).</p><p>解决方案：重新启动provider和consumer……</p><hr><p>9、异常处理 kafka内存不足</p><p>Java HotSpot(TM) 64-Bit Server VM warning: INFO: os::commit_memory(0x00000000c0000000, 1073741824, 0) failed; error&#x3D;’Cannot allocate memory’ (errno&#x3D;12)</p><p>解决方法</p><p>进入kafka_2.11-0.10.0.1&#x2F;bin目录下，修改kafka-server-start.sh文件：</p><p>找到这一行export KAFKA_HEAP_OPTS&#x3D;”-Xmx1G -Xms1G”</p><p>改为   export KAFKA_HEAP_OPTS&#x3D;”-Xmx256M -Xms128M”</p><hr><p>10、Hadoop和HBase中出现 ssh登录　The authenticity of host 192.168.0.xxx can’t be established</p><p>错误是：The authenticity of host 192.168.0.xxx can’t be established.</p><p>执行ssh  -o StrictHostKeyChecking&#x3D;no  192.168.0.xxx</p><hr><p>11、hbaseShell异常（正常情况下hbase启动不是独占！！！）hbase启动后，打开hbase shell用list等命令不好使，报异常（org.apache.hadoop.hbase.PleaseHoldException: Master is initializing）。</p><p>解决方案：1、 ntpdate 0.cn.pool.ntp.org  &#x2F;&#x2F;命令更新master和slave时间</p><p>       2、先去zoo里启动zkServer然后zkCli启动，从里面用rm &#x2F;hbase删除节点</p><p>       3、hdfs(hadoop) dfs -rm -R &#x2F;hbase 删除hdfs中的hbase目录</p><hr><p>12、init测试redis时候出现</p><p><img src="https://s2.ax1x.com/2020/02/04/10xcK1.png"></p><p>很可能原因，是redisTemplate等对象没有自动装配（@Autowired）</p><hr><p>13、login前后端传值Method选择</p><p><img src="https://s2.ax1x.com/2020/02/04/10xWVK.png"></p><p>可能解决方案：把get方法换为post方法</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;1、前端出现此错误(背景：用springMVC做的数据库增删改查小项目，使用gradle管理，前后端分离两模块；时间20190821)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s2.ax1x.com/2020/02/04/10xyvR.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;问</summary>
      
    
    
    
    <category term="框架学习" scheme="https://blog.lebear.top/categories/%E6%A1%86%E6%9E%B6%E5%AD%A6%E4%B9%A0/"/>
    
    
  </entry>
  
</feed>
