.Net Core Configuration源碼探究

前言

    上篇文章我們演示了為Configuration添加Etcd數據源,並且了解到為Configuration擴展自定義數據源還是非常簡單的,核心就是把數據源的數據按照一定的規則讀取到指定的字典里,這些都得益於微軟設計的合理性和便捷性。本篇文章我們將一起探究Configuration源碼,去了解Configuration到底是如何工作的。

ConfigurationBuilder

    相信使用了.Net Core或者看過.Net Core源碼的同學都非常清楚,.Net Core使用了大量的Builder模式許多核心操作都是是用來了Builder模式,微軟在.Net Core使用了許多在傳統.Net框架上並未使用的設計模式,這也使得.Net Core使用更方便,代碼更合理。Configuration作為.Net Core的核心功能當然也不例外。
    其實並沒有Configuration這個類,這隻是我們對配置模塊的代名詞。其核心是IConfiguration接口,IConfiguration又是由IConfigurationBuilder構建出來的,我們找到IConfigurationBuilder源碼大致定義如下

public interface IConfigurationBuilder
{
    IDictionary<string, object> Properties { get; }

    IList<IConfigurationSource> Sources { get; }

    IConfigurationBuilder Add(IConfigurationSource source);

    IConfigurationRoot Build();
}

Add方法我們上篇文章曾使用過,就是為ConfigurationBuilder添加ConfigurationSource數據源,添加的數據源被存放在Sources這個屬性里。當我們要使用IConfiguration的時候通過Build的方法得到IConfiguration實例,IConfigurationRoot接口是繼承自IConfiguration接口的,待會我們會探究這個接口。
我們找到IConfigurationBuilder的默認實現類ConfigurationBuilder大致代碼實現如下

public class ConfigurationBuilder : IConfigurationBuilder
{
    /// <summary>
    /// 添加的數據源被存放到了這裏
    /// </summary>
    public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();

    public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();

    /// <summary>
    /// 添加IConfigurationSource數據源
    /// </summary>
    /// <returns></returns>
    public IConfigurationBuilder Add(IConfigurationSource source)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }
        Sources.Add(source);
        return this;
    }

    public IConfigurationRoot Build()
    {
        //獲取所有添加的IConfigurationSource里的IConfigurationProvider
        var providers = new List<IConfigurationProvider>();
        foreach (var source in Sources)
        {
            var provider = source.Build(this);
            providers.Add(provider);
        }
        //用providers去實例化ConfigurationRoot
        return new ConfigurationRoot(providers);
    }
}

這個類的定義非常的簡單,相信大家都能看明白。其實整個IConfigurationBuilder的工作流程都非常簡單就是將IConfigurationSource添加到Sources中,然後通過Sources里的Provider去構建IConfigurationRoot。

Configuration

通過上面我們了解到通過ConfigurationBuilder構建出來的並非是直接實現IConfiguration的實現類而是另一個接口IConfigurationRoot

ConfigurationRoot

通過源代碼我們可以知道IConfigurationRoot是繼承自IConfiguration,具體定義關係如下

public interface IConfigurationRoot : IConfiguration
{
    /// <summary>
    /// 強制刷新數據
    /// </summary>
    /// <returns></returns>
    void Reload();

    IEnumerable<IConfigurationProvider> Providers { get; }
}

public interface IConfiguration
{
    string this[string key] { get; set; }

    /// <summary>
    /// 獲取指定名稱子數據節點
    /// </summary>
    /// <returns></returns>
    IConfigurationSection GetSection(string key);

    /// <summary>
    /// 獲取所有子數據節點
    /// </summary>
    /// <returns></returns>
    IEnumerable<IConfigurationSection> GetChildren();
    
    /// <summary>
    /// 獲取IChangeToken用於當數據源有數據變化時,通知外部使用者
    /// </summary>
    /// <returns></returns>
    IChangeToken GetReloadToken();
}

接下來我們查看IConfigurationRoot實現類ConfigurationRoot的大致實現,代碼有刪減

public class ConfigurationRoot : IConfigurationRoot, IDisposable
{
    private readonly IList<IIConfigurationProvider> _providers;
    private readonly IList<IDisposable> _changeTokenRegistrations;
    private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();

    public ConfigurationRoot(IList<IConfigurationProvider> providers)
    {
        _providers = providers;
        _changeTokenRegistrations = new List<IDisposable>(providers.Count);
        //通過便利的方式調用ConfigurationProvider的Load方法,將數據加載到每個ConfigurationProvider的字典里
        foreach (var p in providers)
        {
            p.Load();
            //監聽每個ConfigurationProvider的ReloadToken實現如果數據源發生變化去刷新Token通知外部發生變化
            _changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
        }
    }

    //// <summary>
    /// 讀取或設置配置相關信息
    /// </summary>
    public string this[string key]
    {
        get
        {
            //通過這個我們可以了解到讀取的順序取決於註冊Source的順序,採用的是後來者居上的方式
            //后註冊的會先被讀取到,如果讀取到直接return
            for (var i = _providers.Count - 1; i >= 0; i--)
            {
                var provider = _providers[i];
                if (provider.TryGet(key, out var value))
                {
                    return value;
                }
            }
            return null;
        }
        set
        {
            //這裏的設置只是把值放到內存中去,並不會持久化到相關數據源
            foreach (var provider in _providers)
            {
                provider.Set(key, value);
            }
        }
    }

    public IEnumerable<IConfigurationSection> GetChildren() => this.GetChildrenImplementation(null);

    public IChangeToken GetReloadToken() => _changeToken;

    public IConfigurationSection GetSection(string key)
        => new ConfigurationSection(this, key);

    //// <summary>
    /// 手動調用該方法也可以實現強制刷新的效果
    /// </summary>
    public void Reload()
    {
        foreach (var provider in _providers)
        {
            provider.Load();
        }
        RaiseChanged();
    }

    //// <summary>
    /// 強烈推薦不熟悉Interlocked的同學研究一下Interlocked具體用法
    /// </summary>
    private void RaiseChanged()
    {
        var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
        previousToken.OnReload();
    }
}

上面展示了ConfigurationRoot的核心實現其實主要就是兩點

  • 讀取的方式其實是循環匹配註冊進來的每個provider里的數據,是後來者居上的模式,同名key后註冊進來的會先被讀取到,然後直接返回
  • 構造ConfigurationRoot的時候才把數據加載到內存中,而且為註冊進來的每個provider設置監聽回調

ConfigurationSection

其實通過上面的代碼我們會產生一個疑問,獲取子節點數據返回的是另一個接口類型IConfigurationSection,我們來看下具體的定義

public interface IConfigurationSection : IConfiguration
{
    string Key { get; }

    string Path { get; }

    string Value { get; set; }
}

這個接口也是繼承了IConfiguration,這就奇怪了分明只有一套配置IConfiguration,為什麼還要區分IConfigurationRoot和IConfigurationSection呢?其實不難理解因為Configuration可以同時承載許多不同的配置源,而IConfigurationRoot正是表示承載所有配置信息的根節點,而配置又是可以表示層級化的一種結構,在根配置里獲取下來的子節點是可以表示承載一套相關配置的另一套系統,所以單獨使用IConfigurationSection去表示,會顯得結構更清晰,比如我們有如下的json數據格式

{
  "OrderId":"202005202220",
  "Address":"銀河系太陽系火星",
  "Total":666.66,
  "Products":[
    {
      "Id":1,
      "Name":"果子狸",
      "Price":66.6,
      "Detail":{
          "Color":"棕色",
          "Weight":"1000g"
      }
    },
    {
      "Id":2,
      "Name":"蝙蝠",
      "Price":55.5,
      "Detail":{
          "Color":"黑色",
          "Weight":"200g"
      }
    }
  ]
}

我們知道json是一個結構化的存儲結構,其存儲元素分為三種一是簡單類型,二是對象類型,三是集合類型。但是字典是KV結構,並不存在結構化關係,在.Net Corez中配置系統是這麼解決的,比如以上信息存儲到字典中的結構就是這種

Key Value
OrderId 202005202220
Address 銀河系太陽系火星
Products:0:Id 1
Products:0:Name 果子狸
Products:0:Detail:Color 棕色
Products:1:Id 2
Products:1:Name 蝙蝠
Products:1:Detail:Weight 200g

如果我想獲取Products節點下的第一條商品數據直接

IConfigurationSection productSection = configuration.GetSection("Products:0")

類比到這裏的話根配置IConfigurationRoot里存儲了訂單的所有數據,獲取下來的子節點IConfigurationSection表示了訂單里第一個商品的信息,而這個商品也是一個完整的描述商品信息的數據系統,所以這樣可以更清晰的區分Configuration的結構,我們來看一下ConfigurationSection的大致實現

public class ConfigurationSection : IConfigurationSection
{
    private readonly IConfigurationRoot _root;
    private readonly string _path;
    private string _key;

    public ConfigurationSection(IConfigurationRoot root, string path)
    {
        _root = root;
        _path = path;
    }

    public string Path => _path;

    public string Key
    {
        get
        {
            return _key;
        }
    }

    public string Value
    {
        get
        {
            return _root[Path];
        }
        set
        {
            _root[Path] = value;
        }
    }

    public string this[string key]
    {
        get
        {
            //獲取當前Section下的數據其實就是組合了Path和Key
            return _root[ConfigurationPath.Combine(Path, key)];
        }
        set
        {
            _root[ConfigurationPath.Combine(Path, key)] = value;
        }
    }
    
    //獲取當前節點下的某個子節點也是組合當前的Path和子節點的標識Key
    public IConfigurationSection GetSection(string key) => _root.GetSection(ConfigurationPath.Combine(Path, key));
    //獲取當前節點下的所有子節點其實就是在字典里獲取包含當前Path字符串的所有Key
    public IEnumerable<IConfigurationSection> GetChildren() => _root.GetChildrenImplementation(Path);
    public IChangeToken GetReloadToken() => _root.GetReloadToken();
}

這裏我們可以看到既然有Key可以獲取字典里對應的Value了,為何還需要Path?通過ConfigurationRoot里的代碼我們可以知道Path的初始值其實就是獲取ConfigurationSection的Key,說白了其實就是如何獲取到當前IConfigurationSection的路徑。比如

//當前productSection的Path是 Products:0
IConfigurationSection productSection = configuration.GetSection("Products:0");
//當前productDetailSection的Path是 Products:0:Detail
IConfigurationSection productDetailSection = productSection.GetSection("Detail");
//獲取到pColor的全路徑就是 Products:0:Detail:Color
string pColor = productDetailSection["Color"];

而獲取Section所有子節點
GetChildrenImplementation來自於IConfigurationRoot的擴展方法

internal static class InternalConfigurationRootExtensions
{
    //// <summary>
    /// 其實就是在數據源字典里獲取Key包含給定Path的所有值
    /// </summary>
    internal static IEnumerable<IConfigurationSection> GetChildrenImplementation(this IConfigurationRoot root, string path)
    {
        return root.Providers
            .Aggregate(Enumerable.Empty<string>(),
                (seed, source) => source.GetChildKeys(seed, path))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .Select(key => root.GetSection(path == null ? key : ConfigurationPath.Combine(path, key)));
    }
}

相信講到這裏,大家對ConfigurationSection或者是對Configuration整體的思路有一定的了解,細節上的設計確實不少。但是整體實現思路還是比較清晰的。關於Configuration還有一個比較重要的擴展方法就是將配置綁定到具體POCO的擴展方法,該方法承載在ConfigurationBinder擴展類了,由於實現比較複雜,也不是本篇文章的重點,有興趣的同學可以自行查閱,這裏就不做探究了。

總結

    通過以上部分的講解,其實我們可以大概的將Configuration配置相關總結為兩大核心抽象接口IConfigurationBuilder,IConfiguration,整體結構關係可大致表示成如下關係

    配置相關的整體實現思路就是IConfigurationSource作為一種特定類型的數據源,它提供了提供當前數據源的提供者ConfigurationProvider,Provider負責將數據源的數據按照一定的規則放入到字典里。IConfigurationSource添加到IConfigurationBuilder的容器中,後者使用Provide構建出整個程序的根配置容器IConfigurationRoot。通過獲取IConfigurationRoot子節點得到IConfigurationSection負責維護子節點容器相關。這二者都繼承自IConfiguration,然後通過他們就可以獲取到整個配置體系的數據數據操作了。

    以上講解都是本人通過實踐和閱讀源碼得出的結論,可能會存在一定的偏差或理解上的誤區,但是我還是想把我的理解分享給大家,希望大家能多多包涵。如果有大家有不同的見解或者更深的理解,可以在評論區多多留言。

歡迎掃碼關注我的公眾號 本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

探索ADC的原理(自製3位并行比較型ADC)

摘要

      本文通過列舉歷史中出現的產品,梳理了模數轉換器在20世紀30年代~~20世紀80年代末的發展歷史。接下來,簡要介紹模數轉換器的原理、技術指標、分類和未來發展方向。最後,提供了一種自製3位FLASH型ADC的方法(該方法經過了作者的測試且價格在20元以下)。

 

參考文獻

    涉及到的數據手冊(eyg7)

    Flash ADC_Chapter 13 – Digital-Analog Conversion

    ZepToBars

    《Analog-Digital Conversion》 Chapter I Walt Kester

    《数字电子技術》第六版 康華光

    数字电子技術 西南石油大學課程中心

 

ADC的歷史

     世界上記載的第一個”純电子“的A/D轉換器於1939年被亞歷克·哈利·里夫斯(Alec Harley Reeves)發明,該設計的採樣率為6KSPS,分辨率為5位。

亞歷克·哈利·里夫斯設計的A/D轉換器原理圖,《Analog-Digital Conversion》 Chapter I Walt Kester

     1947年,鍺晶體管於貝爾實驗室誕生。

     1946年,ENIAC問世,現代数字計算機的鼻祖,為A/D的蓬勃發展做鋪墊。

     1948年,貝爾實驗室發明了5位、8KSPS的逐次逼近型A/D轉換器。

     得力於电子束編碼管技術,在1960年左右出現了12MSPS、9位的編碼器(A/D)。

 电子束編碼管原理圖,《Analog-Digital Conversion》 Chapter I Walt Kester

     1954年,硅晶體管於德州儀器誕生。

     1954年,伯納德·M·戈登(Bernard M. Gordon)發明了11位、50KSPS的基於真空管的A/D,這被認為是世界上第一個商業化的A/D轉換器。“Datrac”功率500W,售價8000~~9000美元。

伯納德·M·戈登發明的 “Datrac”,《Analog-Digital Conversion》 Chapter I Walt Kester

     1958/1959,集成電路問世,德州儀器(1958),仙童半導體(1959)。

     1963~1965年,為了給美國軍方的雷達提供高速A/D,貝爾實驗室的John M. Eubanks和Robert C. Bedingfield研發了8位、10MSPS的A/D,其功率為150W、售價10000美元。

 John M. Eubanks和Robert C. Bedingfield研發的A/D,《Analog-Digital Conversion》 Chapter I Walt Kester

      1969年,Pastoriza公司利用分立元件製造了12位、10us、2.3W的逐次逼近型A/D樣機—-“ADC-12U”,售價800美元。

 “ADC-12U”原型機,《Analog-Digital Conversion》 Chapter I Walt Kester

     1978年,Paul Brokaw設計了第一個完整的單芯片ADC,型號為AD571,使用了雙極型工藝,參數為:10位、25us、SAR結構。同年,誕生了最具重要意義的SAR ADC–AD574。這時的A/D可以說開始走向現代。

AD571,源

AD571框圖,《Analog-Digital Conversion》 Chapter I Walt Kester

     1988年,Crystal Semiconductor推出了世界上第一個單芯片商業化的ε-Δ ADC–CSZ5316,參數:16位、20KSPS,可以用於語音處理。

     接下來的歷史中,各廠商不斷改進ADC的性能、推出更多不同用途的ADC。總而言之,就是讓ADC進入千家萬戶。

 

ADC的原理

     ADC(Analog to Digital Converter)是一類將模擬信號(連續信號)轉換為数字信號(離散信號)的器件,按原理可分為:并行比較型A/D轉換器(FLASH ADC)、逐次比較型A/D轉換器(SAR ADC)和雙積分式A/D轉換器(Double Integral ADC)。

     模擬信號,下圖中的ui(t)是一個輸入的模擬電壓信號,可以想象成從一個麥克風輸出的音頻信號。

     数字信號,現代計算機能夠處理的信號,表現為下圖中的“n位数字量輸出”。

     香農-奈奎斯特(Shannon & Nyquist)採樣定理規定,使恢復出的信號不失真的條件:採樣頻率大於原始信號頻率的兩倍,即 Fs >= 2Fi

      一個連續的電壓信號ui(t)通過一個由方波CPs控制的開關S之後施加到電容C上,由於電容兩端的電壓不會突變,可知在S斷開時C將維持ui(t)在開關斷開瞬間的電壓一段時間,直到開關S再次打開。這樣,一個模擬的電壓信號就轉換成了採樣展寬信號us(t),其中CPs的頻率就是採樣頻率Fs。然後,由ADC的数字編碼電路將採樣展寬信號us(t)轉換成n位的数字量dn-1 : d0並輸出。

     通過上述步驟,一個連續的電壓信號就轉換成了n位的数字量,而實現該過程的器件叫做模擬-数字轉換器(ADC)。

AD轉換的一般原理,”数字电子技術”  SWPU

TLC5540I,8位、40MSPS、CMOS工藝的并行比較型A/D轉換器的版圖,https://zeptobars.com/,license: CC BY 3.0,未修改

 

 ADC的主要性能指標

     分辨率:ADC能分辨的最小電壓,通常用位數表示,例如:8位。一個n=8位的ADC,參考電壓為5V,則其能分辨的最小電壓為 5 / 2^n = 19.53mV

     轉換時間:ADC從控制信號到來開始,到輸出端得到穩定的数字信號所經歷的時間。

     轉換精度:ADC輸出的数字量所表示的模擬值與實際輸入的模擬量之間的偏差。

 

ADC的分類

     并行比較型A/D轉換器:這是本文嘗試構建的ADC,其由電阻分壓器、電壓比較器(運算放大器)、D觸發器和優先級編碼器構成。其原理簡單,將在後文介紹。

                            優點:1.轉換時間最短,其轉換周期為通過比較器、觸發器和優先級編碼器的時間總和(見下式),這個數值通常很小。

T轉 = T比 + T寄 + T編

                            缺點:1.造價高昂,隨着分辨位數的提高,所需的元件幾乎按幾何級數增長,如:一個n位的并行比較型ADC,需要2^n – 1個比較器和2^n – 1個觸發器,假如n=12,那麼一共需要8190個比較器和觸發器!

                                    2.對集成電路的工藝要求很高。

                    常見的型號:AD9012,TTL工藝,分辨率為8位,採樣率為100MSPS,模擬輸入電壓範圍 -Vs~~+0.5V(Vs為芯片供電電壓)。

                                     AD9002,ECL工藝(射極耦合邏輯),分辨率為8位,採樣率為150MSPS,模擬輸入電壓範圍 -Vs~~+0.5V(Vs為芯片供電電壓)。

                                     AD9020,TTL工藝,分辨率為10位,採樣率為60MSPS,雙極性模擬輸入(+-1.75V)。

3位并行比較型A/D轉換器原理圖,《数字电子技術》第六版 康華光

AD9012原理圖,Analog Devices 

AD9002原理圖,Analog Devices  

AD9020原理圖,Analog Devices 

1107PV2,蘇聯,8位、20MSPS,典型的并行比較型A/D轉換器的版圖https://zeptobars.com/,license: CC BY 3.0,未修改

1107PV2,蘇聯,8位、20MSPS,典型的并行比較型A/D轉換器的比較器的版圖https://zeptobars.com/,license: CC BY 3.0,未修改

 

     逐次比較型A/D轉換器:原理像天平,對輸入的模擬電壓信號與不同權值的電壓做多次比較,使得轉換所得的数字量在數值上不斷逼近輸入的模擬量。通常由控制邏輯電路、數據寄存器、移位寄存器、D/A轉換器(Digital Analog Converter)和電壓比較器構成。

                            優點:1.轉換速度快。其轉換周期等於 分辨率 * 時鐘周期(見下式),如一個8位的逐次比較型A/D轉換器,時鐘周期為10us,則其轉換周期為80us。

T轉 = n * Tclk  (n為分辨率)

                   常見的型號:1.ADC0808/ADC0809,8位逐次比較型A/D轉換器,轉換時間100us,輸入電壓範圍0~~5V,可接入8個模擬量輸入。

                                    2.ADC0803/ADC0804,8位逐次比較型A/D轉換器,在1MHz的時鐘頻率下,轉換時間在66~~73us之間,支持一對差分模擬電壓輸入。

逐次比較型A/D轉換器原理圖 ,《数字电子技術》第六版 康華光

 

 ADC0808/ADC0809原理圖,National Semiconductor

ADC0803/ADC0804原理圖,Philips Semiconductors

 

     雙積分式A/D轉換器:一種間接的A/D轉換器,其分別對輸入電壓和參考電壓進行兩次積分,將輸入電壓平均值變換成與之成正比的時間間隔,然後利用時鐘脈衝和計數器測出此時間間隔,進而在輸出端得到與模擬量相應的数字量。通常由積分器(運算放大器及相應的外部電路)、過零比較器(運算放大器)、時鐘脈衝控制門和計數器等構成。

                         優點:1.抗工頻干擾能力強。通過對輸入電壓的平均值進行變換來實現抗干擾。 

                         缺點:1.轉換速度最慢。

                常見的型號:TLC7135,4.5位雙積分式A/D轉換器,CMOS工藝,差分電壓輸入。

雙積分式A/D轉換器原理圖,《数字电子技術》第六版 康華光

TLC7135数字部分原理圖,Texas Instruments

 

ADC的未來發展方向

     ADC在未來會變得性能更強、價格更低、功耗更低、通用性和專業性更強。

     性能:從歷史上看,對ADC性能的改進主要集中在改進架構改善製造工藝兩個方面。ADC有很多架構,典型的包括:FLASH、SAR和雙積分;其他的有:流水線等。在集成電路發展的過程中,出現了許多的工藝:雙極性、ECL、CMOS、CB、BiCMOS、GaAs……這些工藝可以幫助改進ADC的性能。

     價格:隨着集成電路工藝的不斷成熟,價格變低只是時間問題。

     功耗:得力於集成電路工藝的改善,如:使用更低線寬的IC的功耗會低於高線寬的IC。功耗同時也取決於ADC架構。

 

元件清單(” * “為可選)

————————————————————時鐘發生器部分——————————————————————–

  NE555           *1

  *DIP-8芯片座  *1

  8位撥碼開關    *1

  *3pin排針       *1

  3.9K電阻        *1

  68K電阻         *1

  10uF無極電容   *1

  1uF無極電容     *1

  100nF無極電容 *1

  10nF無極電容   *2

  1nF無極電容     *1

  100pF無極電容 *1

  10pF無極電容   *1

  1pF無極電容     *1

所有元件合照(時鐘發生器部分)

———————————————————————————————————————————————–

————————————————————數模轉換器部分——————————————————————–

  MCP6004        *1(可以使用LM324替換)

  *DIP-14芯片座 *1

  CD4042B *1

  CD4532B *1

  *DIP-16芯片座 *2

  2K可調電阻器   *1

  330R電阻   *3

  390R電阻   *1

  1K電阻       *5

  LED-G       *3

  *Pin-3排母 *1

  *Pin-2排針 *1

所有元件合照(數模轉換器部分,不含Pin-2排針)

———————————————————————————————————————————————–

 

電路原理

 

總原理圖(1)

 

總原理圖(2)

————————————————————時鐘發生器部分——————————————————————–

     555定時器工作在多諧振蕩器模式,通過撥碼開關選擇不同的電容來產生不同頻率的方波。

 

基於555定時器的時鐘發生器原理圖

————————————————————————————————————————————————

————————————————————數模轉換器部分——————————————————————–

     比較器:左側的電阻分壓網絡為右側的四個比較器的反相輸入端提供階梯狀的參考電壓(4V、3V、2V、1V),可調電阻模擬輸入到四個比較器同相端的模擬電壓(0~~5V)。比較器通過比較同相輸入端與反相輸入端電壓的大小,輸出0V(Vp<Vn)或5V(Vp>Vn)給後面的D鎖存器。

 

運放的特性圖

     四路D鎖存器:在時鐘的每一個上升沿,將四個運放輸出的電壓(比較結果)存儲起來並交給後面的編碼器。

     優先級編碼器:對來自鎖存器的四個比較結果進行編碼,並輸出給計算機處理(如果有計算機的話)。

———————————————————————————————————————————————–

 

集成電路簡介

     MCP6004:微芯公司生產的低功耗1MHz帶寬的4路運算放大器,本項目的運算放大器均工作在飽和區。

 

MCP6004實物圖

MCP6004引腳定義

     CD4042B:CMOS四路D鎖存器,在本項目中使用上升沿觸發,時鐘由555定時器提供,用於保存MCP6004輸出的4位數據。

 

CD4042B實物圖

CD4042B引腳定義

CD4042B真值表

     CD4532B: CMOS的8位優先級編碼器,用於對CD4042B鎖存的數據進行編碼。

 

CD4532B實物圖

CD4532B引腳定義

CD4532B真值表

 

測試

 ————————————————————時鐘發生器部分——————————————————————–

     此555時鐘發生電路,實際測試可以產生1Hz、10Hz、100Hz、1KHz、10KHz、100KHz、0.4MHz、0.7MHz的方波信號。實測中,產生的0.4MHz和0.7MHz與設計的1MHz、10MHz存在較大的誤差,可能是電容的問題(這兩個頻率對應所使用的是貼片電容)。

時鐘發生器(正面)

時鐘發生器(反面)

實測產生的最大頻率的波形(Vcc=5V下,Vpp=4.7V)

————————————————————————————————————————————————

————————————————————數模轉換器部分——————————————————————–

     在時鐘為400KHz下,此并行比較型A/D可以正常工作;使用700KHz的時鐘會導致轉換故障。

     主要參數:A/D分辨率為3位(嚴格來說只有2位,可以在不改變架構的情況下通過增加4個比較器拓增至3位)

                   採樣率為400KSPS~~700KSPS。

     功耗:20mA@5V = 100mW (包含時鐘發生器部分)

 

當輸入電壓為2.5V時,輸出的情況(可以看出LED指示“101”,正好是對“1100”編碼的結果)

 

正面(1)

 

正面(2)

 

反面

———————————————————————————————————————————————–

 

聲明

     此教程未經DLHC允許,禁止轉載。所有引用均註明了出處。DLHC保留所有權利。

     由於本人學識有限且整理較為倉促,如有錯誤或不妥,請指正。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

《Java核心技術(卷1)》筆記:第7章 異常、斷言和日誌

異常

  1. (P 280)異常處理需要考慮的問題:

    • 用戶輸入錯誤
    • 設備錯誤
    • 物理限制
    • 代碼錯誤
  2. (P 280)傳統的處理錯誤的方法是:返回一個特殊的錯誤碼,常見的是返回-1或者null引用

  3. (P 280)在Java中,方法出現錯誤時,它會立即退出,不返回任何值,而是拋出一個封裝了錯誤信息的對象

  4. (P 280)Java中所有的異常都是由Throwable繼承而來,它下面又分解為兩個分支:ErrorException

    • Error:描述了Java運行時系統的內部錯誤資源耗盡錯誤(對於處理這種錯誤,你幾乎無能為力
    • Exception:又分解為兩個分支:RuntimeException(由編程錯誤導致,如果出現該異常,那一定是你的問題)和其他異常(諸如IO錯誤這類問題)
      • 派生於RuntimeException的異常:
        • 錯誤的強制類型轉換
        • 數組訪問越界
        • 訪問null指針
      • 不是派生於RuntimeException的異常:
        • 試圖超越文件末尾繼續讀取數據
        • 試圖打開一個不存在的文件
        • 試圖根據特定的字符串查找Class對象,而這個字符串表示的類並不存在

    graph TD Throwable[Throwable]–>Error[Error] Throwable[Throwable]–>Exception[Exception] Error[Error]–>OtherError1[…] Error[Error]–>OtherError2[…] Error[Error]–>OtherError3[…] Exception[Exception]–>RuntimeException[RuntimeException] Exception[Exception]–>IOException[IOException] Exception[Exception]–>OtherException[…] IOException[IOException]–>OtherIOException1[…] IOException[IOException]–>OtherIOException2[…] IOException[IOException]–>OtherIOException3[…] RuntimeException[RuntimeException]–>OtherRuntimeException1[…] RuntimeException[RuntimeException]–>OtherRuntimeException2[…] RuntimeException[RuntimeException]–>OtherRuntimeException3[…]

  5. (P 281)派生於ErrorRuntimeException的所有異常稱為非檢查型異常,所有其他異常稱為檢查型異常

  6. (P 282)如果沒有處理器捕獲異常對象,那麼當前執行的線程就會終止

  7. (P 283)必須在方法的首部列出所有檢查型異常類型,但是不需要聲明從Error繼承的異常,也不應該聲明從RuntimeException繼承的那些非檢查型異常

  8. (P 283)如果在子類中覆蓋了超類的一個方法,子類方法中聲明的檢查型異常不能比超類方法中聲明的異常更通用(子類方法可以拋出更特定的異常,或者根本不拋出任何異常)。如果超類方法沒有拋出任何檢查型異常,子類也不能拋出任何檢查型異常

  9. (P 288)同一個catch子句中可以捕獲多個異常類型,如果一些異常的處理邏輯是一樣的,就可以合併catch子句。只有當捕獲的異常類型彼此之間不存在子類關係時才需要這個特性

    try {
        ...
    } catch (FileNotFoundException | UnknownHostException e) {
        ...
    } catch (IOException e) {
        ...
    }
    
  10. (P 289)可以在catch子句中拋出一個異常,此時,可以把原始異常設置為新異常的“原因”

    try {
        ...
    } catch (SQLException original) {
        var e = new ServletException("database error");
        e.initCause(original);
        throw e;
    }
    

    捕獲異常時,獲取原始異常

    Throwable original = caughtException.getCause();
    
  11. (P 292)一種推薦的異常捕獲寫法:內層try語句塊只有一個職責,就是確保釋放資源外層try語句塊也只有一個職責,就是確保報告出現的錯誤

    try {
        try {
            ...
        } finally {
            // 釋放資源
        }
    } catch (Exception e) {
        // 報告錯誤
    }
    
  12. (P 293)Java 7中,對於實現了AutoCloseable接口的類,可以使用帶資源的try語句(try-with-resources):

    try (Resources res = ...) {
        // Work with res
        ...
    }
    

    Java 9中,可以在try首部中提供之前聲明的事實最終變量(effectively final variable):

    try (res) {
        // Work with res
        ...
    } // res.close() called here
    
  13. (P 294)在try-with-resources語句中,如果try塊拋出一個異常,而且close方法也拋出一個異常,則原來的異常會重新拋出,而close方法拋出的異常會“被抑制”(可以通過getSuppressed方法得到這些被抑制的異常)

  14. (P 294)可以通過StackWalker類處理堆棧軌跡

    var walker = StackWalker.getInstance();
    walker.forEach(frame -> ...); // 例如:walker.forEach(System.out::println);
    
  15. (P 298)使用異常的一些技巧:

    • 異常處理不能代替簡單的測試(捕獲處理異常的成本很高,只在異常情況下使用異常
    • 不要過分的細化異常(有必要將整個任務包在一個try語句塊中,將正常處理與錯誤處理分開
    • 充分利用異常層次結構
    • 不要壓制異常(異常非常重要時,應該適當地進行處理)
    • 在檢測錯誤時,“苛刻”要比放任更好
    • 不要羞於傳遞異常(最好繼續傳遞異常,而不是自己捕獲)

斷言

  1. (P 301)Java中引入了關鍵字assert,其有如下兩種形式:

    assert condition;
    assert condition : expression;
    

    這兩個語句都會計算條件,如果結果為false,則拋出一個AssertionError異常。在第二個語句中,表達式將傳入AssertionError對象的構造器,並轉換為一個消息字符串

  2. (P 301)默認情況下,斷言是禁用的,可以使用-enableassertions或者-ea選項啟用斷言:

    java -enableassertions MyApp
    

    禁用斷言可以使用-disableassertions-da

  3. (P 302)斷言只應該用於在測試階段確定程序內部錯誤的位置

日誌

  1. (P 305)基本日誌的使用:

    • 生成簡單的日誌記錄

      Logger.getGlobal().info("hello world!");
      
    • 取消所有日誌

      Logger.getGlobal().setLevel(Level.OFF);
      
  2. (P 305)高級日誌的使用:

    • 創建或獲取日誌記錄器

      private static final Logger myLogger = Logger.getLogger("className"); // className是全限定類名
      
    • 設置日誌級別

      logger.setLevel(Level.FINE); // FINE以及更高級別的日誌都會被記錄
      
    • 記錄日誌

      // 調用相應級別的日誌記錄方法
      logger.warning(message);
      logger.fine(message);
      
      // 使用log方法並指定級別
      logger.log(Level.FINE, message);
      
      // 跟蹤執行流的方法
      logger.entering("className", "methodName", new Object[]{ params... });
      logger.exiting("className", "methodName", result);
      
      // 在日誌記錄中包含異常的描述
      logger.throwing("className", "methodName", exception);
      logger.log(Level.WARNING, message, exception);
      
  3. (P 305)7個日誌級別:

    • SEVERE
    • WARNING
    • INFO
    • CONFIG
    • FINE
    • FINER
    • FINEST
  4. (P 307)可以通過配置文件修改日誌系統的各個屬性,默認情況下,配置文件位於:

    conf/logging.properties
    

    指定特定位置的配置文件:

    java -Djava.util.logging.config.file=configFile MainClass
    

    指定日誌記錄器的日誌級別:在日誌記錄器名後面追加後綴.level,例如

    com.mycompany.myapp.level=FINE
    
  5. (P 313)日誌技巧

    • 對一個簡單的應用,選擇一個日誌記錄器,可以把日誌記錄器命名為與主應用包一樣的名字
    • 默認的日誌配置會把級別等於或高於INFO的所有消息記錄到控制台,用戶可以覆蓋這個默認配置,最好在你的應用中安裝一個更合適的默認配置
    • 所有級別為INFO、WARNING和SEVERE的消息都將显示到控制台上
      • 只將對程序用戶有意義的消息設置為以上這幾個級別
      • 程序員想要的日誌消息設定為FINE級別是一個很好的選擇
  6. (P 321)調試技巧

    • 打印或日誌記錄變量的值

    • 在每一個類中放置一個單獨的main方法,以便獨立地測試類

    • 使用JUnit

    • 日誌代理,它是一個子類的對象,可以截獲方法調用,記錄日誌,然後調用超類中的方法

      var generator = new Random() {
          public double nextDouble() {
              double result = super.nextDouble();
              Logger.getGlobal().info("nextDouble: " + result);
              return result;
          }
      }
      
    • 利用Throwable類的printStackTrace方法,可以從任意的異常對象獲得堆棧軌跡

    • 一般來說,堆棧軌跡显示在System.err上。如果想要記錄或显示堆棧軌跡,可以將它捕獲到一個字符串中

      var out = new StringWriter();
      new Throwable().printStackTrace(new PrintWriter(out));
      String description = out.toString();
      
    • 通常,將程序錯誤記入一個文件會很有用:

      java MyProgram > errors.txt        # 錯誤,錯誤被發送到System.err而不是System.out
      java MyProgram 2> errors.txt       # 正確,只輸出System.err
      java MyProgram 1> errors.txt 2>&1  # 同時捕獲System.out和System.err
      
    • 將未捕獲的異常的堆棧軌跡記錄到一個文件中,而不是直接輸出到System.err,可以使用靜態方法Thread.setDefaultUncaughtExceptionHandler改變未捕獲異常的處理器

      Thread.setDefaultUncaughtExceptionHandler(
          new Thread.UncaughtExceptionHandler() {
              public void uncaughtException(Thread t, Throwable e) {
                  // save information in log file
              }
          }
      )
      
    • 要想觀察類的加載過程,啟動Java虛擬機時可以使用-verbose標誌

    • -Xlint選項告訴編譯器找出常見的代碼問題

      javac -Xlint sourceFiles
      
    • Java虛擬機增加了對Java應用程序的監控和管理支持,允許在虛擬機中安裝代理來跟蹤內存消耗、線程使用、類加載等情況。jconsole工具可以显示有關虛擬機性能的統計結果

    • Java任務控制器:一個專業級性能分析和診斷工具

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

跳躍表確定不了解下

redis源碼分析系列文章

[Redis源碼系列]在Liunx安裝和常見API 

為什麼要從Redis源碼分析 

String底層實現——動態字符串SDS 

Redis的雙向鏈表一文全知道

面試官:說說Redis的Hash底層 我:……(來自閱文的面試題)

前言

hello,大家好,周五見了。前面幾周我們一起看了Redis底層數據結構,如動態字符串SDS,雙向鏈表Adlist,字典Dict,如果有對Redis常見的類型或底層數據結構不明白的請看上面傳送門。

今天我們來看下ZSET的底層架構,如果不知道ZSET是什麼的,可以看上面傳送門第一篇。簡單來說,ZSET是Redis提供的根據數據和分數來判斷其排名的數據結構。最常見的就是微信運動的排名,每個用戶對應自己的步數,每天晚上可以給出用戶的排名。

有小夥伴可能會想,如果是實現排名的話,各種排序方法都可以實現的,沒必要引入Redis的ZSET結構啊?

當然,如果是採用排序方法的話,是可以實現相同功能的,但是代碼裏面需要硬編碼,會添加工作量,還會提供代碼的Bug哦,哈哈哈。而且Redis的底層是C實現的,直接操作內存,速度也會比Java方法實現提升。

綜上,使用Redis的ZSET結構,好處多多。那話不多說,開始把。在正式開始之前,我們需要引入下跳躍表的概念,其是ZSET結構的底層實現。以下可能有點枯燥,我盡量說的簡單點哈。

什麼是跳躍表?

對於數據量大的鏈表結構,插入和刪除比較快,但是查詢速度卻很慢。那是因為無法直接獲取某個節點,需要從頭節點開始,藉助某個節點的next指針來獲取下一節點。即使數據是有序排放的,想要查詢某個數據,只能從頭到尾遍歷變量,查詢效率會很低,時間複雜度為O(n)。

如果我們需要快速查詢鏈表有啥辦法呢?有同學說用數組存放,但是如果不改數據結構呢?

我們可以先想想在有序數組結構中有二分法,每次將範圍都縮小一半,這樣查詢速度提升了很多,那麼在鏈表中能不能也使用這種思想。

這就到了今天講的主角——跳躍表。(一點也生硬的引出概念)

步驟一  新建有序單項鏈表

先看下圖有序單向鏈表,存放了1,2,3,4,5,6,7這7個元素。

步驟二 抽取二級索引節點

我們可以在鏈表中抽取部分節點,下圖抽取了1,3,5,7四個節點,也就是每兩個節點提取了一個節點到上級,抽取出來的叫做索引。

注意不是每次都能抽取到這麼完美,這其實就跟拋硬幣一樣,每個硬幣的正反兩面的概率是一樣的,都是1/2。當數據量小的時候,正反的概率可能差別較大。但是隨着數據量的加大,正反的概率越來越接近於1/2。類比過來是一個意思,每個節點的機會都是一樣的,要麼停留原級,要麼提取到上級,概率都是1/2。但是隨着節點數量的增加,抽取的節點越來越接近與1/2。

步驟三 抽取三級索引節點

我們可以在鏈表中抽取部分節點,下圖抽取了1,5兩個節點,也就是每兩個節點提取了一個節點到上級,抽取出來的叫做索引。

步驟四 類二分法查詢

我們假設要查找值為6的節點,先從三級索引開始,找到值為1的節點,發現比5小,根據值為1節點的next指針,找到值為5的節點,5後面沒有其他的三級索引啦。

於是順着往下找,到了二級索引,根據值為5的節點的next指針找到值為7的節點,發現比6小,說明要找到的節點6在此範圍內。

再接着到了一級索引位置,根據值為5的節點next指針指向值為6的節點,發現是想要查詢的數據,所以查詢過程結束。

根據上面的查詢過程(下圖的藍色連線),我們發現其採用的核心思想是二分法,不斷縮小查詢範圍,如果在上層索引找到區間,則順延深入到下一層找到真正的數據。

總結

從上面的整個過程中可以看出,數據量小的時候,這種拿空間換時間,消耗內存方法的並不是最優解。所以Redis的zset結構在數據量小的時候採用壓縮表,數據量大的時候採用跳躍表。

像這種鏈表加多級索引的結構,就是跳躍表。這名字起的形象,過程是跳躍着來查詢的。

Redis中跳躍表圖解

下圖簡單來說是對跳躍表的改進和再封裝,首先引入了表頭的概念,這與雙向鏈表,字典結構一樣,都是對數據的封裝,因為他們都是採用的指針,而指針必然導致在計算長度,獲取最後節點的數據問題上會產生查詢太慢的性能問題,所以封裝表頭是為了在這些問題上提升速度,浪費的只是添加,刪除等操作的時間,與此對比,是可以忽略的。

其次是引入管理所有節點的層數數組,我們可以看到有32層,即32個數組,這和後面的數據節點結構是一樣的。引入它是為了便於直接根據此數組的層數定位到每個元素。

再其次是數據節點的每個level都有層級和span(也就是下圖箭頭指針上的数字,其是為了方便統計兩個節點相距多少長度)。

最後就是數據節點的後退指針backward,引入目的是Level數組只有前指針,即只能指向下一個節點地址,而後退指針是為了能往回找節點。

上圖主要分為3大塊:(這邊大致看下就行,下面將對各模塊進行代碼詳細解釋)

表頭

主要包括四個屬性,分別是頭指針header,尾指針tail,節點長度length,所有節點的最大level。

header:指向跳躍表的表頭節點,通過這個指針地址可以直接找到表頭,時間複雜度為O(1)。

tail:指向跳躍表的表尾節點,通過這個指針可以直接找到表尾,時間複雜度為O(1)。

length:記錄跳躍表的長度,即不包含表頭節點,整個跳躍表中有多少個元素。

level:記錄當前跳躍表內,所有節點層數最大的level(排除表頭節點)。

管理所有節點層數level的數組

其對象值為空,level數組為32層,目的是為了管理真正的數據節點。關於具體的level有哪些屬性放在數據節點來說。

數據節點

主要包括四個屬性對象值obj,分數score,後退指針backward和level數組。每個數據的Level數組有多少層,是隨機產生的,這跟上面說過的跳躍表是一樣的。

成員對象obj:真正的實際數據,每個節點的數據都是唯一的,但是節點的分數可能相同。兩個相同分數的節點是按照成員對象在字典中的大小進行排序的,成員對象較小的節點會排在前面,成員對象較大的節點會排在後面。

分數score:各個節點中的数字是節點所保存的分數,在跳躍表中,節點按各自所保存的分數從小到大排列。

後退指針backward:用於從表尾向表頭遍歷,每個節點只有一個後退指針,即每次只能後退一步。

層級level:節點中用1,2,3等字樣標記節點的各個層,L1代表第一層,L2代表第二層,L3代表第三層,並以此類推。

跳躍表的定義

表頭結構zskiplist

 

typedef struct zskiplist {
    //表頭的頭指針header和尾指針tail
    struct zskiplistNode *header, *tail;
    //一共有多少個節點length
    unsigned long length;
    // 所有節點最大的層級level
   int level;
} zskiplist;

具體數據節點zskiplistNode

 

//跳錶的具體節點 
typedef struct zskiplistNode {
    sds ele; //具體的數據,對應張三
    double score;//分數,對應70
    struct zskiplistNode *backward;//後退指針backward
     //層級數組    struct zskiplistLevel {
        struct zskiplistNode *forward;//前進指針forward
        unsigned int span;//跨度span
    } level[];
} zskiplistNode; 

 

跳躍表的實現(源碼分析)

redis關於跳躍表的API都定義在t_zset.c文件中。

千萬不要看到源碼分析就跑開了,一定要看哦。

創建跳躍表

創建空的跳躍表,其實就是創建表頭和管理所有的節點的level數組。首先,定義一些變量,嘗試分配內存空間。其次是初始化表頭的level和length,分別賦值1和0。接着創建管理所有節點的Level的數組,是調用zslCreateNode函數,輸入參數為數組大小宏常量ZSKIPLIST_MAXLEVEL(32),分數為0,對象值為NULL。(此為跳躍表得以實現重點)。再接着就是為此數組每個元素的前指針forword和跨度span初始化。最後初始化尾指針並返回值。

可以參照下面的圖解和源碼:

 

//創建一個空表頭的跳躍表
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;
    //嘗試分配內存空間
    zsl = zmalloc(sizeof(*zsl));
    //初始化level和length
    zsl->level = 1;
    zsl->length = 0;
    //調用下面的方法zslCreateNode,傳入的參數有數組長度ZSKIPLIST_MAXLEVEL 32
    //分數0,對象值NuLL
    //這一步就是創建管理所有節點的數組
    //並且設置表頭的頭頭指針為此對象的地址
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
    //為這32個數組賦值前指針forward和跨度span
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
        zsl->header->level[j].forward = NULL;
        zsl->header->level[j].span = 0;
    }
    //設置尾指針
    zsl->header->backward = NULL;
    zsl->tail = NULL;
    //返回對象
    return zsl;
}
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
    zskiplistNode *zn =
        zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
    zn->score = score;
    zn->ele = ele;
    return zn;
}

 

插入節點

比如有下圖6個元素,需要插入值為趙六,分數為101的元素,我們大致想一想,大致的步驟包括找到要插入的位置新建一個數據節點,然後調整與之相關的頭尾指針的level數組。那就看看redis咋做的,和我們想的一樣不一樣呢?

噔噔噔噔,答案揭曉。當然了大框架是相同的。

正文開始了:(先來圖片)

1.遍歷管理所有節點的level數組,從最大的level開始,即3,挨個對比值,如果有分數比他大的值或者分數相同,但是數據的值比他大,記錄到數組裡面,同時記錄跨度。

這樣說太抽象了。拿上圖舉個例子,從表頭的level即3開始,首先到張三的L3,發現分數70,比目標分數101小跳過,根據其前指針找到趙六的L3,發現分數102,比目標分數101大,將趙六L3記錄在待更新數組update中,同時記錄跨度span為4。接着到下一層,張三的L2層,發現分數70比目標分數101小跳過,根據前指針找到王五的L2,發現分數90,比目標分數101小跳過,根據前指針找到趙六的L2,發現分數102比目標分數101大,將趙六的L2記錄到待更新數組update中,同時記錄跨度span為2。最後到下一層,張三的L1層,邏輯和剛才一樣的,也是記錄趙六的L1層和跨度span為1。

2.為新節點隨機生成層級數level(通過位運算),如果生成的level大於目前level最大值3,則將將大於部分挨個遍歷,並將跨度等信息記錄到上面update表中。

比如,新節點生成的level為5,目前level最大值為3,說明這個節點只會有一個,並且跨越了之前的所有節點,那麼我們將從第四層和第五層都遍歷下,記錄到待更新數組update中。

3.準備工作都做好了,找到了該節點將插入到哪一位置,處於哪一層,每層對應的跨度是多少,下面就要新增數據節點了。把上兩步的信息都添加到新節點上,並且調整位置前後指針即可。

4.最後就是一些收尾工作,比如修改表頭的層級level,節點大小length和尾指針tail等屬性。

綜上,整個流程就已經結束了。可能看着有點複雜,可以對照下面代碼來。

 

//插入節點,輸入參數為
//zsl:表頭
//score:插入元素的分數score
//ele:插入元素的具體數據ele
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    //使用update數組記錄每層待插入元素的前一個元素
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    //記錄前置節點與第一個節點之間的跨度,即元素在列表中的排名-1
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    serverAssert(!isnan(score));
    x = zsl->header;
    //從最大的level開始遍歷,從頂到底,找到每一層待插入的位置
    for (i = zsl->level-1; i >= 0; i--) {
        /* store rank that is crossed to reach the insert position */
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
    //直接找到第一個分數比該元素大的位置
    //或者分數與該元素相同但是對象的ASSICC碼比該元素大的位置
        while (x->level[i].forward &&
                (x->level[i].forward->score < score ||
                    (x->level[i].forward->score == score &&
                    sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            //將已走過元素的跨越元素進行計數,得到元素在列表中排名,或者是已搜尋的路徑長度
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
    //記錄待插入位置
        update[i] = x;
    }
     //隨機產生一個層數,在1到32之間,層數越高,生成的概率越低
    level = zslRandomLevel();
    //如果產生的層數大於現有的最高層數,則超出層數都需要初始化
    if (level > zsl->level) {
        //開始循環
        for (i = zsl->level; i < level; i++) {
            rank[i] = 0;
            //該元素作為這些層的第一個節點,前節點就是header
            update[i] = zsl->header;
            //初始化后這些層每層有兩個元素,走一步就是跨越所有元素
            update[i]->level[i].span = zsl->length;
        }
        zsl->level = level;
    }
    //創建節點
    x = zslCreateNode(level,score,ele);
    for (i = 0; i < level; i++) {
        //將新節點插入到各層鏈表中
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;

        // rank[0]是第0層的前置節點P1(也就是底層插入節點前面那個節點)與第一個節點的跨度
        // rank[i]是第i層的前置節點P2(這一層里在插入節點前面那個節點)與第一個節點的跨度
        // 插入節點X與後置節點Y的跨度f(X,Y)可由以下公式計算
        // 關鍵在於f(P1,0)-f(P2,0)+1等於新節點與P2的跨度,這是因為跨度呈扇形形向下延伸到最底層
        // 記錄節點各層跨越元素情況span, 由層與層之間的跨越元素總和rank相減而得
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
               // 插入位置前一個節點的span在原基礎上加1即可(新節點在rank[0]的后一個位置)

 update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

    /* increment span for untouched levels */
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }
    // 第0層是雙向鏈表, 便於redis常支持逆序類查找
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        zsl->tail = x;
    zsl->length++;
    return x;
}

 

int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

 

獲取節點排名

擔心大家忘了這張圖,再粘貼一遍。如下圖,這部分邏輯比較簡單,就不寫了,具體參考代碼分析。

 

//得到節點的排名
//輸入參數為表頭結構zsl,分數score,真正的數據ele
unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *x;
    unsigned long rank = 0;
    int i;
    //先獲取表頭的頭指針,即找到管理所有節點的level數組
    x = zsl->header;
     //從表頭的level,即最大值開始循環遍歷
    for (i = zsl->level-1; i >= 0; i--) {
        //如果找到分數小於目標分數的,排名加上其跨度
        //或者分數相同,但是具體數據小於目標數據的,排名也加上跨度
        while (x->level[i].forward &&
            (x->level[i].forward->score < score ||
                (x->level[i].forward->score == score &&
                sdscmp(x->level[i].forward->ele,ele) <= 0))) {
            rank += x->level[i].span;
            x = x->level[i].forward;
        }

        //確保在第i層找到分值相同,且對象相同時才會返回排位值
        if (x->ele && sdscmp(x->ele,ele) == 0) {
            return rank;
        }
    }
    return 0;
}

 

結語

該篇主要講了Redis的ZSET數據類型的底層實現跳躍表,先從跳躍表是什麼,引出跳躍表的概念和數據結構,剖析了其主要組成部分,進而通過多幅過程圖解釋了Redis是如何設計跳躍表的,最後結合源碼對跳躍表進行描述,如創建過程,添加節點過程,獲取某個節點排名過程,中間穿插例子和過程圖。

如果覺得寫得還行,麻煩給個贊,您的認可才是我寫作的動力!

如果覺得有說的不對的地方,歡迎評論指出。

好了,拜拜咯。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

用一個圖書庫實例搞懂二分搜索樹的底層原理

目錄

  • 一、背景
  • 二、概念
    • 1、定義
    • 2、 動畫示例
  • 三、圖書庫實例
    • 3.1、項目需求
    • 3.2、代碼結構
    • 3.3、圖書類
    • 3.4、二分搜索樹的底層實現
    • 3.5、圖書庫的構建
  • 四、深入理解

一、背景

二叉樹是一種常用的數據結構,更是實現眾多算法的一把利器。本文將通過建立一個圖書庫的實例對二叉樹中的常用類型:二分搜索樹(Binary Search Tree)進行底層原理的深入理解。

二、概念

1、定義

1 二分搜索樹是一顆二叉樹
2 二分搜索樹每個節點的左子樹的值都小於該節點的值,每個節點右子樹的值都大於該節點的值
3 任意一個節點的每棵子樹都滿足二分搜索樹的定義

2、 動畫示例

三、圖書庫實例

3.1、項目需求
  • 創建一個圖書類:圖書類中需包含ISBN號,書名,作者,定價,出版社、出版日期等
  • 用二分搜索樹的數據結構創建一個圖書庫,每種圖書需有當前數量
  • 圖書庫需實現添加圖書,遍歷整個圖書庫,及可根據ISBN號進行快速查找
3.2、代碼結構

3.3、圖書類
  • 在圖書類的定義中,重寫compareTo方法:通過比較ISBN(國際標準書號)的大小表示圖書在二叉樹的結點順序。
/**
 - 用二分搜索樹實現圖書庫--圖書類
 -  - @author zhuhuix
 - @date 2020-06-23
 */
public class Books implements Serializable, Comparable {
    // ISBN
    private Long bookId;
    // 作者
    private String author;
    // 分類
    private String category;
    // 書名
    private String bookName;
    // 定價
    private BigDecimal bookPrice;
    // 出版社
    private String bookPublisher;
    // 出版時間
    private LocalDate bookDate;
    // 當前數量
    private Integer bookCount;

    public Books(Long bookId, String bookName, String category, String author, BigDecimal bookPrice, String bookPublisher, LocalDate bookDate, Integer bookCount) {
        this.bookId = bookId;
        this.author = author;
        this.category = category;
        this.bookName = bookName;
        this.bookPrice = bookPrice;
        this.bookPublisher = bookPublisher;
        this.bookDate = bookDate;
        this.bookCount = bookCount;
    }

    public Books(Long bookId){
        this.bookId= bookId;
    }

    // 通過ISBN號進行比較大小
    @Override
    public int compareTo(Object o) {
        if (o instanceof Books) {
            return this.getBookId().compareTo(((Books) o).getBookId());
        } else {
            return -1;
        }
    }

    public Long getBookId() {
        return bookId;
    }


    public Integer getBookCount() {
        return bookCount;
    }

    public void setBookCount(Integer bookCount) {
        this.bookCount += bookCount;
    }

    @Override
    public String toString() {
        return "{" +
                "ISBN=" + bookId +
                ", 書名='" + bookName + '\'' +
                ", 作者='" + author + '\'' +
                ", 分類='" + category + '\'' +
                ", 價格=" + bookPrice +
                ", 出版社='" + bookPublisher + '\'' +
                ", 出版時間=" + bookDate +
                ", 當前數量=" + bookCount +
                '}';
    }
}
3.4、二分搜索樹的底層實現
  • 底層創建內部結點類(class Node):元素,左子樹,右子樹
  • add方法:使用遞歸方法增加結點:
    如果圖書種類不存在,則創建新結點。
    如果圖書種類存在,則對數量進行累加。
  • traverse方法:使用遞歸方法對所有結點進行遍歷
  • search方法:根據ISBN碼查找結點
/**
 * 用二分搜索樹實現圖書庫--二分搜索樹
 *
 * @author zhuhuix
 * @date 2020-06-23
 */
public class BinarySearchTree {

    // 結點
    private Node root;
    // 書的種類
    private int bookSize;
    // 書的總數量
    private int bookCount;

    public BinarySearchTree() {
        this.root = null;
        this.bookSize = 0;
        this.bookCount = 0;
    }

    // 增加元素
    public void add(Books data) {
        this.root = addNode(this.root, data);
    }

    // 用遞歸方法實現結點的添加
    private Node addNode(Node node, Books data) {
        // 遞歸退出條件 書不存在拉加結點,並將結點數量加1
        if (node == null) {
            this.bookSize++;
            this.bookCount += data.getBookCount();
            return new Node(data);
        }

        if (node.data.compareTo(data) < 0) {
            node.right = addNode(node.right, data);
        } else if (node.data.compareTo(data) > 0) {
            node.left = addNode(node.left, data);
        } else if (node.data.compareTo(data) == 0) {
            // 如果結點已存在,則將書的數量累加
            this.bookCount += data.getBookCount();
            node.getData().setBookCount(data.getBookCount());
        }
        return node;
    }

    // 用遞歸方法實現結點前序遍歷
    public void traverse(Node node) {
        if (node == null) {
            return;
        }
        System.out.println(node.getData().toString());
        traverse(node.left);
        traverse(node.right);
    }

    // 用遞歸方法實現通過isbn查找圖書
    public Books search(Long isbn) {
        Node node = nodeSearch(this.root, new Books(isbn));
        if (node != null) {
            return node.getData();
        } else {
            return null;
        }
    }

    private Node nodeSearch(Node node, Books books) {
        if (node == null) {
            return null;
        }
        if (books.compareTo(node.getData()) == 0) {
            return node;
        } else if (books.compareTo(node.getData()) < 0) {
            return nodeSearch(node.left, books);
        } else {
            return nodeSearch(node.right, books);
        }
    }

    public Node getRoot() {
        return root;
    }

    // 返回書的種類數
    public int getBookSize() {
        return bookSize;
    }

    // 返回書的總數量
    public int getBookCount() {
        return bookCount;
    }

    // 私有內部類-樹結點
    private class Node {
        Books data;
        Node left, right;

        Node(Books data) {
            this.data = data;
            this.left = null;
            this.right = null;
        }

        Books getData() {
            return data;
        }
    }
}

3.5、圖書庫的構建
  1. 構建一棵二分搜索樹;
  2. 將京東十大暢銷圖書加入二分搜索樹;
  3. 統計圖書種類及數量,並遍歷輸出;
  4. 加入3種已經進入圖書庫的圖書;
  5. 再次統計圖書種類及數量,並遍歷輸出;
  6. 根據某個ISBN號查找圖書。
/**
 * 用二分搜索樹實現圖書庫
 *
 * @author zhuhuix
 * @date 2020-06-23
 */
public class BookStore {
    public static void main(String[] args) {

        // 構建一棵二分搜索樹
        BinarySearchTree bst = new BinarySearchTree();

        // 將十大暢銷圖書加入二分搜索樹
        bst.add(new Books(9787115428028L,"Python編程 從入門到實踐",
                "編程語言與程序設計","埃里克·馬瑟斯",
                BigDecimal.valueOf(61.40),"人民郵電出版社",
                LocalDate.of(2017,07,01),1));

        bst.add(new Books(9787115525963L,"說服力 工作型PPT該這樣做",
                "辦公軟件","秦陽",
                BigDecimal.valueOf(66.30),"人民郵電出版社",
                LocalDate.of(2020,05,01),1));

        bst.add(new Books(9787569222258L,"零基礎學Python(全彩版)",
                "編程語言與程序設計","明日科技",
                BigDecimal.valueOf(67.00),"吉林大學出版社",
                LocalDate.of(2018,04,01),1));

        bst.add(new Books(9787121388361L,"PS之光:一看就懂的Photoshop攻略(全彩)",
                "圖形圖像/多媒體","馮注龍",
                BigDecimal.valueOf(60.70),"电子工業出版社",
                LocalDate.of(2020,06,01),1));

        bst.add(new Books(9787302423287L,"機器學習",
                "人工智能","周志華",
                BigDecimal.valueOf(64.80),"清華大學出版社",
                LocalDate.of(2016,01,01),1));

        bst.add(new Books(9787111641247L,"深入理解Java虛擬機:JVM高級特性與最佳實踐(第3版)",
                "編程語言與程序設計","周志明",
                BigDecimal.valueOf(106.40),"机械工業出版社",
                LocalDate.of(2019,12,01),1));

        bst.add(new Books(9787115472588L,"鳥哥的Linux私房菜 基礎學習篇 第四版",
                "操作系統","鳥哥",
                BigDecimal.valueOf(93.00),"人民郵電出版社",
                LocalDate.of(2018,10,01),1));

        bst.add(new Books(9787115293800L,"算法(第4版)",
                "編程語言與程序設計","Robert Sedgewick,Kevin Wayne",
                BigDecimal.valueOf(66.30),"人民郵電出版社",
                LocalDate.of(2012,10,01),1));

        bst.add(new Books(9787115537973L,"數學之美 第三版",
                "計算機理論、基礎知識","吳軍",
                BigDecimal.valueOf(54.40),"人民郵電出版社",
                LocalDate.of(2020,05,01),1));

        bst.add(new Books(9787302255659L,"大話數據結構",
                "編程語言與程序設計","程傑",
                BigDecimal.valueOf(47.20),"清華大學出版社",
                LocalDate.of(2011,06,01),1));

        // 遍歷圖書庫
        System.out.println("圖書庫新建:");
        System.out.println("書的種類數:"+bst.getBookSize());
        System.out.println("書的總數量:"+bst.getBookCount());
        bst.traverse(bst.getRoot());

        // 再次增加相同的書
        bst.add(new Books(9787302255659L,"大話數據結構",
                "編程語言與程序設計","程傑",
                BigDecimal.valueOf(47.20),"清華大學出版社",
                LocalDate.of(2011,06,01),1));

        bst.add(new Books(9787115472588L,"鳥哥的Linux私房菜 基礎學習篇 第四版",
                "操作系統","鳥哥",
                BigDecimal.valueOf(93.00),"人民郵電出版社",
                LocalDate.of(2018,10,01),1));

        bst.add(new Books(9787115293800L,"算法(第4版)",
                "編程語言與程序設計","Robert Sedgewick,Kevin Wayne",
                BigDecimal.valueOf(66.30),"人民郵電出版社",
                LocalDate.of(2012,10,01),1));

        // 再次遍歷圖書庫
        System.out.println("圖書庫同種圖書加入:");
        System.out.println("書的種類數:"+bst.getBookSize());
        System.out.println("書的總數量:"+bst.getBookCount());
        bst.traverse(bst.getRoot());

        // 根據ISBN號查找圖書
        Books books =bst.search(9787115472588L);
        if (books!=null) {
            System.out.println("已找到該圖書:");
            System.out.println(books.toString());
        }
    }
}

程序輸出如下:

圖書庫新建:
書的種類數:10
書的總數量:10
{ISBN=9787115428028, 書名='Python編程 從入門到實踐', 作者='埃里克·馬瑟斯', 分類='編程語言與程序設計', 價格=61.4, 出版社='人民郵電出版社', 出版時間=2017-07-01, 當前數量=1}
{ISBN=9787111641247, 書名='深入理解Java虛擬機:JVM高級特性與最佳實踐(第3版)', 作者='周志明', 分類='編程語言與程序設計', 價格=106.4, 出版社='机械工業出版社', 出版時間=2019-12-01, 當前數量=1}
{ISBN=9787115293800, 書名='算法(第4版)', 作者='Robert Sedgewick,Kevin Wayne', 分類='編程語言與程序設計', 價格=66.3, 出版社='人民郵電出版社', 出版時間=2012-10-01, 當前數量=1}
{ISBN=9787115525963, 書名='說服力 工作型PPT該這樣做', 作者='秦陽', 分類='辦公軟件', 價格=66.3, 出版社='人民郵電出版社', 出版時間=2020-05-01, 當前數量=1}
{ISBN=9787115472588, 書名='鳥哥的Linux私房菜 基礎學習篇 第四版', 作者='鳥哥', 分類='操作系統', 價格=93.0, 出版社='人民郵電出版社', 出版時間=2018-10-01, 當前數量=1}
{ISBN=9787569222258, 書名='零基礎學Python(全彩版)', 作者='明日科技', 分類='編程語言與程序設計', 價格=67.0, 出版社='吉林大學出版社', 出版時間=2018-04-01, 當前數量=1}
{ISBN=9787121388361, 書名='PS之光:一看就懂的Photoshop攻略(全彩)', 作者='馮注龍', 分類='圖形圖像/多媒體', 價格=60.7, 出版社='电子工業出版社', 出版時間=2020-06-01, 當前數量=1}
{ISBN=9787115537973, 書名='數學之美 第三版', 作者='吳軍', 分類='計算機理論、基礎知識', 價格=54.4, 出版社='人民郵電出版社', 出版時間=2020-05-01, 當前數量=1}
{ISBN=9787302423287, 書名='機器學習', 作者='周志華', 分類='人工智能', 價格=64.8, 出版社='清華大學出版社', 出版時間=2016-01-01, 當前數量=1}
{ISBN=9787302255659, 書名='大話數據結構', 作者='程傑', 分類='編程語言與程序設計', 價格=47.2, 出版社='清華大學出版社', 出版時間=2011-06-01, 當前數量=1}
圖書庫同種圖書加入:
書的種類數:10
書的總數量:13
{ISBN=9787115428028, 書名='Python編程 從入門到實踐', 作者='埃里克·馬瑟斯', 分類='編程語言與程序設計', 價格=61.4, 出版社='人民郵電出版社', 出版時間=2017-07-01, 當前數量=1}
{ISBN=9787111641247, 書名='深入理解Java虛擬機:JVM高級特性與最佳實踐(第3版)', 作者='周志明', 分類='編程語言與程序設計', 價格=106.4, 出版社='机械工業出版社', 出版時間=2019-12-01, 當前數量=1}
{ISBN=9787115293800, 書名='算法(第4版)', 作者='Robert Sedgewick,Kevin Wayne', 分類='編程語言與程序設計', 價格=66.3, 出版社='人民郵電出版社', 出版時間=2012-10-01, 當前數量=2}
{ISBN=9787115525963, 書名='說服力 工作型PPT該這樣做', 作者='秦陽', 分類='辦公軟件', 價格=66.3, 出版社='人民郵電出版社', 出版時間=2020-05-01, 當前數量=1}
{ISBN=9787115472588, 書名='鳥哥的Linux私房菜 基礎學習篇 第四版', 作者='鳥哥', 分類='操作系統', 價格=93.0, 出版社='人民郵電出版社', 出版時間=2018-10-01, 當前數量=2}
{ISBN=9787569222258, 書名='零基礎學Python(全彩版)', 作者='明日科技', 分類='編程語言與程序設計', 價格=67.0, 出版社='吉林大學出版社', 出版時間=2018-04-01, 當前數量=1}
{ISBN=9787121388361, 書名='PS之光:一看就懂的Photoshop攻略(全彩)', 作者='馮注龍', 分類='圖形圖像/多媒體', 價格=60.7, 出版社='电子工業出版社', 出版時間=2020-06-01, 當前數量=1}
{ISBN=9787115537973, 書名='數學之美 第三版', 作者='吳軍', 分類='計算機理論、基礎知識', 價格=54.4, 出版社='人民郵電出版社', 出版時間=2020-05-01, 當前數量=1}
{ISBN=9787302423287, 書名='機器學習', 作者='周志華', 分類='人工智能', 價格=64.8, 出版社='清華大學出版社', 出版時間=2016-01-01, 當前數量=1}
{ISBN=9787302255659, 書名='大話數據結構', 作者='程傑', 分類='編程語言與程序設計', 價格=47.2, 出版社='清華大學出版社', 出版時間=2011-06-01, 當前數量=2}
已找到該圖書:
{ISBN=9787115472588, 書名='鳥哥的Linux私房菜 基礎學習篇 第四版', 作者='鳥哥', 分類='操作系統', 價格=93.0, 出版社='人民郵電出版社', 出版時間=2018-10-01, 當前數量=2}

四、深入理解

  1. 二分搜索樹的底層是一個鏈點,可以實現高效地插入,刪除以及動態維護。
  2. 二分搜索樹的結點是有序的,可以很快地求出最大,最小之類的關係值。
  3. 也正是因為二分搜索樹的結點是有序的,在極端情況下,二分搜索樹會褪化成一個鏈表

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

CCNA-Part1:網絡基礎概念

由於身處一家網絡公司,日常項目中涉及到的網絡概念較多,恰逢之後公司組織相關培訓。藉此機會,打算寫下一系列文章用於之后梳理並回顧。文章主要涉及 NA,NP 中所覆蓋的知識。由於網絡分為較多方向,如路由交換,無線,安全等。在今年,大綱正好有所改變,其中無線和路由交換放在一起合稱為企業架構。所以本系列文章以企業架構為主。

在網絡界,Cisco 證書一直被普遍認可,其中分為三個等級 NA,NP,IE. 對於開發人員來說,掌握 NA 水平一般即可,本系列文章會 NA 開始,到 NP 結束。NA 內容較為寬泛,其中涉及知識面較寬,但不深入,用於入門。NP 在 NA 基礎上,更加深入,涉及到更多的協議與概念。

話不多說,在閱讀本文後,應該了解以下內容:

  1. 了解網絡的組成?
  2. 衡量網絡的指標?
  3. 不同應用程序對網絡的要求?

什麼是網絡?

網絡的組成

先看一下網絡的組成組件:

主要分為 4 大類:

  • 終端設備:產生和接受數據的設備,如 PC ,服務器,電話
  • 中間設備:常見叫法為2,3層設備,如 2 層設備交換機,3層設備路由器
  • 媒介:數據傳輸時的媒介,如無線,線纜,光纖。
  • 服務:如應用服務器,taobao,QQ 等

由此可見,網絡就是由上述設備組成,在其中進行數據產生,轉發的過程。在討論網絡時,一般討論的都是企業網絡,下面是常見的一張企業網絡的架構圖。

從圖中我們可以看到,網絡的大體架構由,終端設備,接入層設備(交換機,用於將設備接入),轉發層設備(路由器)和數據中心組成。舉例子來說,如果左下角的同學 A 想要和右上角的同學 B 同學通信,則需要經歷如下過程:

  1. 通過接入層設備接入企業內網
  2. 通過核心層設備,將數據轉發至 ISP(服務提供商,如聯通電信等)
  3. ISP 將數據轉發至同學 B 所在核心層設備。
  4. B 所在核心層設備轉發至接入層設備。
  5. 接入層設備轉發給用戶B。

接口類型

設備連入的端口,稱為接口,下面是常見的接口類型。

接口名稱 速度
Ethernet 10M
Fast Ethernet 100M
GigabitEthernet 1000M
10GigabitEthernet 10000M
40GigabitEthernet 40000M
100GigabitEthernet 1000000M
Serial – 串行口

衡量網絡的主要指標

在討論或者設計一個網絡架構是,往往會在如下的方面進行討論:

帶寬

表示數據的發送速率,單位為比特每秒(b/s),意思為一秒鐘發送的比特數,因此帶寬又稱為比特率。(以太網: 10Mbps, 快速以太網:100Mbps)

可用性與可靠性:

拓展性:

在設計網絡架構時,要考慮到可拓展性,公司的人數會隨着時間增加。

安全性:

數據傳輸和存儲的安全。

服務質量 (Qos):

對流量進行分類整理,拿家庭具體,分類出訪問 QQ 的流量,訪問 P2P 的流量,然後對其進行限制設置上限,防止一個服務佔用過大的帶寬,造成其他服務無法正常訪問。

開銷(Cost):

設備及搭建網絡的費用。

虛擬化:

將一個物理設備虛擬化成多個虛擬設備,例如交換機,路由器等。

拓撲:

物理拓撲:實際設備之間的物理連接的布局,稱為物理拓撲。

物理之間拓撲的比較:

拓撲類型 優點
總線拓撲 1. 所用電纜少
2. 結構簡單
3. 易於擴充
4. 布線方便
1. 傳輸距離有限,通信範圍受到限制
2.故障診斷和隔離較困難
3.所有數據經過總線傳送,不具有實時功能
4. 單點故障:所有 PC 不得不共享線纜,一個節點出錯,將影響整個網絡
環形拓撲 1. 增加或減少工作站時,僅需要簡單的連接操作
2. 電纜長度短
3. 傳輸延遲確定
1. 傳輸距離有限,通信範圍受到限制
2. 故障診斷和隔離困難
3. 節點過多時影響傳輸效率
4. 任意節點出現故障,整個網絡將癱瘓
星型拓撲-局域網較為常見 1. 集中控制,控制簡單
2. 故障診斷和隔離容易
3. 網絡延遲短
1. 中央節點的負擔較重,形成瓶頸
2. 各節點的分佈處理能力較低
3. 網絡共享能里較差
部分網狀拓撲-廣域網常見 1. 系統可靠性高,比較容易擴展 1. 結構複雜,每一結點都與多點進行連結
2. 因此必須採用路由算法和流量控制方法。

邏輯拓撲:以數據轉發的過程為側重,描述節點之間數據的轉發過程。

應用程序流量分類

在網絡中提供服務的應用種類較多,對應對網絡的要求也一般不同,可大致分為如下幾類:

總結

本篇內容中,多為網絡的基礎概念,方便大家入門,只需理解有個印象就好。接下來的內容,才是真正學習的網絡的開始。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

必知必會的8個Python列表技巧

原作者:Nik Piepenbreier

翻譯&內容補充:費弗里

原文地址:https://towardsdatascience.com/advanced-python-list-techniques-c6195fa699a3

  列表(List)是你使用Python過程中接觸最為頻繁的數據結構,也是功能最為強大的幾種數據結構之一。Python列表非常的萬能且蘊含着許多隱藏技巧,下面我們就來探索一些常用的列表技巧。

1 列表元素的過濾

1.1 filter()的使用

  filter()函數接受2個參數:1個函數對象以及1個可迭代的對象,接下來我們定義1個函數然後對1個列表進行過濾。

  首先我們創建1個列表,並且剔除掉小於等於3的元素:

圖1

  回顧一下發生了什麼:

  1. 我們定義了列表original_list
  2. 接着我們定義了一個接受數值型參數number的函數filter_three,當傳入的參數值大於3時會返回True,反之則會返回False
  3. 我們定義了filter對象filtered,其中filter()接受的第一個參數是函數對象,第二個參數是列表對象
  4. 最終我們將filter對象轉化為列表,最終得到經filter_three過濾后original_list內留下的元素。

1.2 使用列表推導式

  類似的,我們也可以利用列表推導式來過濾列表元素,作為一種生成和修改列表優雅的方式,列表推導式想必大家都比較熟悉了,下面是使用列表推導完成同樣任務的過程:

圖2

2 修改列表

2.1 map()的使用

  Python中內置的map()函數使得我們可以將某個函數應用到可迭代對象內每一個元素之上。

  比方說我們想獲取到一個列表對象中每一個元素的平方,就可以使用到map()函數,就像下面的例子一樣:

圖3

  類似filter()的工作過程,下面我們來看看發生了什麼:

  1. 首先我們定義了列表original_list,以及接受數值型參數並返回其平方值的函數square()
  2. 接着我們定義了map對象squares,類似filter()map()接受的第一個參數是函數對象,第二個參數是列表對象
  3. 最終我們將map對象squares列表化,就得到了想要的結果

2.2 使用列表推導式

  同樣的我們也可以使用列表推導式完成同樣的任務:

圖4

3 利用zip()來組合列表

  有些情況下我們需要將兩個或以上數量的列表組合在一起,這類需求使用zip()來完成非常方便。

  zip()函數接收多個列表作為參數傳入,進而得到每個位置上一一對應的元素組合,就像下面的例子一樣:

圖5

4 顛倒列表

  Python中的列表是有序的數據結構,正因如此,列表中元素的順序很重要,有些時候我們需要翻轉列表中所有元素的順序,可以通過Python中的切片操作,用::-1來快捷地實現:

圖6

5 檢查列表中元素的存在情況

  有些情況下我們想要檢查列表中是否存在某個元素,這種時候就可以使用到Python中的in運算符,譬如說我們有一個記錄了所有比賽獲勝隊伍名稱的列表,當我們想查詢某個隊名是否已獲勝時,可以像下面的例子一樣:

圖7

6 找出列表中出現次數最多的元素

  有些情況下我們想要找出列表中出現次數最多的元素,譬如對記錄若干次拋硬幣結果的列表,找出哪一種結果出現次數最多,就可以參考下面的例子:

圖8

7 展平嵌套列表

  有些情況下我們會遇到一些嵌套的列表,其每個元素又是各自不同的列表,這種時候我們就可以利用列表推導式來把這種嵌套列表展平,如下面2層嵌套的例子:

圖9

額外補充

  原作者這裏只考慮到兩層嵌套的列表,如果是更多層嵌套,就需要有多少層寫多少for循環,比較麻煩,其實還有一種更好的方法,我們可以使用pip install dm-tree來安裝tree這個專門用於展平嵌套結構的庫,可以展平任意層嵌套列表,使用例子如下:

圖10

8 檢查唯一性

  如果你想要查看列表中的值是否都是唯一值,可以使用Python中的set數據結構的特點,譬如下面的例子:

圖11

  以上就是本文的全部內容,如有疑問歡迎在評論區討論~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

一分鐘開始持續集成之旅系列之:C 語言 + Makefile

作者:CODING – 朱增輝

前言

make 工具非常強大,配合 makefile 文件可以實現軟件的自動化構建,但是執行 make 命令依然需要經歷手動輸入執行、等待編譯完成、將目標文件轉移到合適位置等過程,我們真正關心的是最終的輸出,卻在這些中間過程上浪費了很多時間。利用 CODING 持續集成功能可以實現自動觸發構建,構建全程自動化,無須分心看護,節省時間。

本文通過一個 C 語言 + Makefile Demo 項目講解如何使用 CODING 持續集成功能創建構建計劃,自動觸發構建,以及如何將生成的目標文件發布到 CODING generic 製品庫。

準備工作

環境

本文涉及到以下工具,請確認已存在,或者根據鏈接的文檔進行安裝。

  • git
  • make
  • gcc

另外,您還需準備一個 CODING 項目。

代碼

我已經準備了一份簡單的示例代碼,使用 make 工具構建 Hello-world 程序。

// hello.c
#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

您可以通過下面的命令克隆到本地。

git clone https://e.coding.net/coding-public/demo-c-make.git

倉庫中還包含了一個 makefile 文件,定義了簡單的規則來完成軟件構建。

all: hello

hello: hello.o
	gcc -o hello hello.o

hello.o: hello.c
	gcc -c hello.c

clean:
	rm -rf hello.o hello

您可以在本地執行 make 命令以驗證構建正常。

下面我們正式開始通過一個 Demo 演示 CODING 平台持續集成功能的使用。

步驟一 創建製品庫

為了方便隨時使用構建出來的目標文件,我們將構建物存儲到 CODING 平台製品庫,因此需要先創建合適的製品倉庫,這裏創建 generic 倉庫比較合適。

從左側導航欄打開製品庫

單擊新建倉庫,選擇 generic 類型,按照提示指定倉庫名稱,這裏倉庫名取為 generic。

步驟二 創建並配置構建計劃

從左側導航欄打開持續集成 --> 構建計劃頁面,點擊新建構建計劃配置創建並配置新的構建計劃。在彈出的頁面中,輸入構建計劃名稱,選擇代碼倉庫,配置來源指的的該構建計劃的構建腳本存放位置,對於簡單的、變動不頻繁的腳本可以使用靜態配置的選項,否則更推薦使用代碼倉庫中的腳本,這樣更加靈活,方便管理

點擊使用模板,可根據自己需要選擇合適模板,這裏選擇 簡易模板

保存構建計劃后,系統會自動將構建模板對應的 Jenkinsfile 推送到倉庫,默認為 master 分支。

步驟三 編寫構建腳本

構建腳本定義構建過程的具體步驟,是構建計劃的核心部分。CODING 平台提供了圖形化編輯器方便您快速編寫構建腳本。

CODING 持續集成底層基於開源 CI/CD 軟件領導者 Jenkins 實現,完全兼容 Jenkins pipeline 構建腳本語法,根據 Jenkins 官方提供的腳本編寫指南,可以實現更複雜的構建任務,CODING 也提供了文本編輯器方便您在線編輯。

代碼倉庫中已包含一個簡單的構建腳本(Jenkisnfile),您可以按照自己的想法參考編寫。

// Jenkinsfile
pipeline {
  agent any
  stages {
    stage('檢出') {
      steps {
        checkout([
          $class: 'GitSCM',
          branches: [[name: env.GIT_BUILD_REF]],
          userRemoteConfigs: [[
            url: env.GIT_REPO_URL,
            credentialsId: env.CREDENTIALS_ID
          ]]])
        }
      }
      stage('構建') {
        steps {
          echo '構建中...'
          sh 'make'
          echo '構建完成.'
        }
      }
      stage('發布') {
        steps {
          echo '發布中...'
          codingArtifactsGeneric(
            files: 'hello',
            repoName: "${env.GENERIC_REPO_NAME}",
            version: "${env.GIT_COMMIT}",
          )
          echo '發布完成'
        }
      }
    }
  }
}

構建腳本中的大部分內容都比較容易理解,稍顯陌生的是 codingArtifactsGeneric 步驟,這是 CODING 官方提供的插件,方便上傳到 CODING generic 製品庫。該插件通過環境變量 GENERIC_REPO_NAME 獲取倉庫名,因此需要配置構建計劃設置該變量值。

步驟四 配置觸發構建規則

CODING 持續功能支持多種觸發方式包括代碼源觸發、定時觸發、API 觸發及手動觸發,這幾種觸發方式可以同時配置互不衝突,其中代碼源觸發又可配置為推送到指定分支或標籤觸發,觸發方式多樣,可滿足絕大部分場景需要。

如前言中所說,我們希望把更多的精力放在源代碼上,盡量減少構建所帶來的干擾,因此這裏必不可少的是配置通過代碼源觸發,通過配置如下正則表達式,可以在推送代碼到匹配的分支名時自動觸發構建。

^refs/(heads/(release|release-.*|build-.*|feat-.*|fix-.*|test-.*|mr/.*))

步驟五 執行構建

執行構建最簡單的方式是手動觸發構建,選中想要構建的構建計劃,單擊立即構建會彈出配置窗口,在這裏可以配置此次構建使用的參數,單擊確定即可開始構建。

按照步驟四的配置,我們的構建計劃也支持推送的匹配分支觸發構建,您可以執行如下命令創建新分支並推送到遠端倉庫,即可觸發構建。

git checkout -b build-ci-test
git push origin HEAD

觸發后,構建會自動執行,您可以繼續做其他事情。

步驟六 下載目標文件

步驟三中定義的構建腳本會將構建出的目標文件發布到 CODING 製品庫,如果我們想要在本地使用也是很方便下載的。在製品倉庫中單擊文件名即可看到指引頁,裏面給出了對文件不同操作的命令。

總結

本文通過一個 C 語言 + makefile 的 Demo 項目講解了 CODING 持續集成、製品庫的簡單使用。藉由 CODING 平台的這些功能,我們像是雇了一個永不會累的助手,承擔了耗時的構建工作,從而節省了時間,提高了效率。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

基於雲服務的個人網站架構設計

本文介紹如何基於各種雲服務優雅且低成本地搭建個人網站,涉及的雲產品有雲服務器、SSL、企業郵箱、對象存儲、CDN、雲函數、API網關、雲監控等。

概述

如今雲服務提供商們提供了大量涵蓋計算、網絡、存儲等方面的雲服務,其中一些雲產品功能強大,如果能善加利用可以大幅降低開發和運維的成本。下面以基於騰訊雲搭建的個人網站為例,對網站整體的架構進行介紹。

網站目前的主要功能是個人博客,後續可以擴展如個人網盤等其他應用。當前架構圖如下:

一、基礎設施

1.雲服務器CVM

雲服務器使用的是CVM,1核2G,下行帶寬1Mbps,這個配置用來搭建起步階段的個人博客是完全夠用了,購買學生機或者在活動時購買價格也比較便宜。

有了服務器資源就可以開始博客搭建,我選的博客系統是極簡主義的Typecho,安裝過程可以參考這篇博文,主要是在服務器上安裝nginx、mysql、php以及typecho的源碼。

2.域名

註冊 – 備案 – 解析

服務器創建后同時會分配一個公網ip,但是為了便於分享和傳播,建議進行域名註冊。註冊后需要進行備案,現在的備案流程也已經簡化為在小程序上操作,省去了原有的幕布拍照環節,前後大概1-2周時間就可以完成備案。之後在控制台進行域名解析,即綁定域名和服務器ip,注意對帶或不帶www前綴的域名都要進行解析,完成解析后就可以在瀏覽器通過域名來訪問網頁了。

主域名的確定

為了便於SEO,建議根據個人喜好確定一個主域名,因為搜索引擎對於帶www和不帶www前綴的地址是當成兩個網站分開計算權重的。國內網站一般帶www,而國外網站(如github、stackoverflow、leetcode)等是不帶www的。我這裡是選擇不帶www的地址(zhayujie.com),並在nginx中配置對帶www的訪問301重定向到不帶www上,以集中權重。

企業郵箱

擁有域名后,還可以註冊以自己域名為後綴的企業郵箱,基礎版免費使用且賬號數量無上限,再也不用擔心郵箱號不夠用了(如微信公眾平台註冊),郵箱格式類似於 zyj@zhayujie.com

3.全站HTTPS

為了網站安全以及利於SEO,建議支持https協議訪問網站。可以申請免費的SSL證書,將證書和私鑰放置到服務器,並在nginx中開啟並配置SSL。同樣為了避免分散權重,可以把http訪問的請求301重定向到https上。以我的網站為例,帶不帶www以及是否使用https都會統一訪問https://zhayujie.com/。

二、基於COS和CDN的圖床

1.對象存儲COS

由於服務器下行帶寬有限,如果圖片存儲於我們自己的服務器,出現併發訪問時可能導致帶寬超限,訪問速度下降。所以可以把圖片存儲到 COS(Cloud Object Storage)中,搭建自己的圖床,這樣當博客同步到其他博客平台時,也便於對圖片資源進行統一管理。

COS的使用比較簡單,類似於網盤,在存儲桶中可以建立樹狀目錄結構,每個存儲桶(bucket)會分配一個公網域名,其下的文件通過https://{bucket}/{dir}/{filename}的形式進行訪問。但在博客中直接使用該鏈接是不妥的,因為一旦我們遷移到其他雲服務商或者切換其他的存儲方式了,原有的鏈接就失效了,一一修改成本太高。好在cos支持配置自定義域名,可以通過類似http://{domain}/{dir}/{filename}的地址進行訪問。

2.內容分髮網絡CDN

COS的自定義源站域名不支持https訪問,為了不影響我們的全站https,並且同時提升訪問速度和減少流量成本,可以配合CDN服務,開啟自定義CDN加速域名,具體步驟見文檔。

可以選取一個子域名作為cdn自定義域名,添加CNAME解析,這樣通過自定義域名會首先訪問cdn邊緣服務器,如果未命中則回源到cos。例如上面的圖片我配置的地址是https://blog.cos.zhayujie.com/web/blog-cloud-arch.jpg。

三、基於Serverless的消息服務

1.雲函數SCF

在博客開發過程中會遇到一些發送消息的功能,比如讀者回復文章時給筆者發送通知,筆者回複評論時給讀者發送通知,博文發布時給訂閱的讀者發送通知等等。這種消息通知的功能是很適合單獨拆分出來形成一個消息服務的,如果寫在博客源碼中則復用性差(網站下其他應用要發送消息時需要重寫),而單獨部署服務又會增加運維的成本(如果服務掛掉怎麼辦),這時候可以考慮serverless(無服務器)的架構,僅將我們的核心代碼片段託管給雲服務商。

騰訊雲提供了雲函數SCF(Serverless Cloud Function),是一種FaaS技術。對於消息通知這種異步、無狀態的功能,很適合使用雲函數編寫,比如接收到請求後向指定接收人發送一封郵件。

2.API網關

雲函數的觸發方式有多種,最常用的有定時任務和API網關。由於消息通知是通過事件觸發而不是定時觸發,所以選擇API網關,創建了觸發器后便可從公網直接訪問該函數,與Nginx反向代理的作用類似。

API網關的域名是隨機生成的,不利於對未來變化的擴展,故同樣綁定自定義域名,使用https://{domain}/{function}形式的地址觸發函數。例如我的郵件發送函數地址配置為https://apigw.zhayujie.com/commentNotice,在業務代碼中只需向該地址發送POST請求即可觸發郵件投遞。

四、監控、快照和統計

1.監控告警

服務器的監控和告警同樣很重要,有助於我們及時發現並排查問題。監控部分一般直接在控制台的 雲服務器 – 實例 – 監控 中進行查看,有對不同時間周期和時間粒度下的CPU、內存、帶寬、磁盤等的詳細數據。

告警部分則在雲監控中配置,可以配置多種報警策略如對cpu、內存、帶寬等指標超出閾值後進行告警,以及一些機器故障事件(如ping不可達、機器重啟等)。對COS的報警同樣可以在此配置。告警渠道可以是微信、郵件和短信。

2.快照

為了防止服務器硬盤中的數據遭到攻擊或被誤刪,可以在 雲服務器 – 快照 控制台中設置進行快照備份,並且支持定期快照策略,設置每隔一段時間自動創建新的快照。

3.訪問統計

對網站的訪問情況進行統計分析有利於我們優化網站內容和體驗。對於訪問數據統計使用的是百度統計,使用埋點方式接入,可以查看每一個訪客的地域,來源,搜索詞,轉化等信息,統計訪問量趨勢。

對於搜索引擎工具使用的是百度站長工具,用於提交頁面收錄,查看索引量、抓取頻次等數據。

總結

以上就是一個功能齊全的個人博客的搭建過程,大致計算一下成本,雲服務器活動期購買一百一年,域名一般幾十塊一個,而COS、CDN、SCF等產品都有大量的免費額度,且在建站初期流量費用同樣是微乎其微,所以總體算下來成本是極低的。個人開發者可以把個人網站當做一個產品來做,思考如何利用好公有雲的各種雲產品資源來提升用戶體驗,提高開發效率,降低運維成本。

原文鏈接:https://zhayujie.com/blog-cloud-arch.html

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

※教你寫出一流的銷售文案?

虹軟人臉識別——官方 Qt Demo 移植到 Linux

一、前言

最近需要在 Linux 平台下開發一個人臉識別相關的應用,用到了虹軟的人臉識別 SDK。之前在 Windows 平台用過,感覺不錯,SDK 裏面還帶了 Demo 可以快速看到效果。打開 Linux 版本的 SDK 裏面沒有發現 Demo,於是想着把 Windows 的 Demo 移植到 Linux。這篇文章記錄了移植的過程,Linux 用的是 Ubuntu 20.04(使用虛擬機 VMware Workstation 15 Player)。

二、配置依賴

2.1 ArcFace SDK

到虹軟官網下載人臉識別 SDK 3.1 Linux 增值版本 解壓到合適的目錄,並從官網獲取 APP_ID、SDK_KEY 和 ACTIVE_KEY,用於寫到配置文件用來激活 SDK。

2.2 OpenCV

到 OpenCV 官網下載源碼,我用的版本是 3.4.9。可以按照官網的教程 Installation in Linux 自行編譯,我參考官網教程使用下面的這些命令在 GCC 9.3.0(Ubuntu 20.04 自帶的編譯器) 上編譯成功。

sudo apt update
sudo apt install build-essential
sudo apt install cmake git libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev
cd <OpenCV 源碼目錄>
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=<自定義目錄> ..
make -j3    # 可以使用核心數 - 1 個線程來編譯
sudo make install

2.3 Qt

Qt 使用的是 5.14.2 版本。

三、項目文件

3.1 .pro 文件

原 Windows Demo 使用的是 Visual Studio 2015,在 Linux 下我這裏用到了 Qt Creator 進行開發,因此需要編寫 .pro 文件,包括以下幾個方面:

  • 用到的 Qt 模塊
  • 編譯出來的程序名
  • 用到的頭文件、源文件和資源文件
  • 依賴庫的頭文件及庫名

更具體的內容查看文末提供的源碼。

3.2 文件編碼

原源碼文件使用的是 GBK 編碼,需要轉換為 UTF-8 編碼。將下面的命令保存到 convert.sh 文件中並用 chmod u+x convert.sh 賦予可執行權限。

#!/bin/bash

for i in "$@"; do
    desc=$(file "$i")
    if $(echo $desc | grep -i "UTF-8 Unicode" > /dev/null); then
        if $(echo $desc | grep -i "(with BOM)" > /dev/null); then
            echo "Remove UTF-8 BOM: " $i
            sed -i "1s/^\xef\xbb\xbf//" "$i"
        fi
    elif $(echo $desc | grep -i "ISO-8859" > /dev/null); then
        echo     "GBK --> UTF-8   : " $i
        temp=temp.txt
        iconv -f gbk -t utf-8 -o "$temp" "$i"
        mv "$temp" "$i"
    fi
done

在源碼根目錄下運行 find . -type f \( -name "*.h" -o -name "*.cpp" \) | xargs -I{} ./convert.sh "{}" 將所有的文件的編碼從 GBK 轉為 UTF-8,並去除現有 UTF-8 文件的 BOM 頭。

四、代碼修改

到這裏已經可以用 Qt Creator 打開項目了,但在代碼中還存在一些問題,一方面是原代碼使用了一些 Windows 平台特有的 API,一方面是有些代碼在 Linux 有兼容性問題。先去除 Windows 特有的依賴到編譯通過,再補充必要的依賴,最後解決兼容性問題。

4.1 修改報錯直至編譯通過

直接進行編譯,逐步解決編譯錯誤,通過下面的方式可以解決編譯錯誤:

  • 刪除 Utils.cpp 中報錯的頭文件、GUID 宏、listDevices 函數的主體、UTF8_To_string 和 string_To_UTF8 函數。
  • 將所有包含的 qDebug 改為 QDebug
  • Sleep(milli) 改為 std::this_thread::sleep_for(std::chrono::milliseconds(milli))
  • 刪除 MSVC 的鏈接庫的編譯指令 #pragma comment ...
  • 將原來使用 OpenCV 2 的接口遷移到目前的 OpenCV 3。
    • IplImage 到 cv::Mat 的轉換由 cv::Mat mat(ipl, false) 改成 cv::Mat mat = cv::cvarrToMat(ipl)
    • cv::Mat 到 IplImage 的轉換由 IplImage(mat) 改成 cvIplImage(mat),在原來的代碼里 cv::Mat 轉為 IplImage 後有個取地址,對右值取地址是不安全的,需要用一個變量保存轉換后的值再對這個變量取地址。
    • 在調用 cvRectangle 時將 CV_RGB 改成 cvScalar
    • 使用 cv::cvtColor 需要額外包含頭文件 opencv2/imgproc.hpp
    • 使用 cv::VideoCapture 需要額外包含頭文件 opencv2/videoio.hpp
  • strcpy_s 改成 strncpy,僅有參數位置上的改變。
  • TRUE 改為 true,將 FALSE 改為 false

改了編譯錯誤后,忽略警告已經可以編譯通過了,接下來是補充剛才刪除的一些必要依賴及解決兼容性問題。

因為環境差異,可能出現錯誤的順序不一致,但基本上是上面提到的錯誤之一。

4.2 重新實現獲取攝像頭列表的函數

原 Windows Demo 使用了 Windows 特有的 dshow 來查找攝像頭,在這裏直接用 cv::VideoCapture 嘗試打開來獲取攝像頭的索引:

auto list = std::vector<int>();
for (auto i = 0; i != 10; ++i)
{
    auto cap = cv::VideoCapture(i);
    if (cap.isOpened()) { list.emplace_back(i); }
    cap.release();
}

Demo 可以只打開一個RGB攝像頭,也可以同時打開一個RGB攝像頭和一個IR攝像頭。原代碼保存獲取攝像頭的名稱,僅用來統計數量,具體打開哪個攝像頭是通過settings.ini文件來配置的。在改變探測攝像頭存在的數量的方式后,順帶改變了打開攝像頭的邏輯,僅一個攝像頭就認為是僅打開普通攝像頭。在settings.ini文件中配置兩種攝像頭的索引,如果索引為 -1,則自動把小的索引認為是普通攝像頭,大的索引認為是紅外攝像頭,如果和真實情況不一致可手動指定攝像頭索引。

settings.ini文件在後面運行 Demo 時會有更多的說明。

4.3 修復彈出文件選擇框失敗的兼容性問題

在 Ubuntu 20.04 下,Qt 的 QFileDialog::getOpenFileName 和 QFileDialog::getExistingDirectory 存在一些問題,在打開時會卡死界面,通過將最後一個參數設置為 QFileDialog::DontUseNativeDialog 可以解決這個問題。

五、運行 Demo

5.1 界面預覽

5.2 配置

  1. 配置文件已經隨源碼打包好了,在運行時需要移動到可執行程序所在的同級目錄下。
  2. 在配置文件中填入官網獲取的 APP_ID、SDK_KEY 和 ACTIVE_KEY。
  3. 編譯並運行。

六、源碼下載

基於官方windows Qt Demo修改后的源碼

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!