標籤: 新北清潔

  • 為什麼用抓包工具看HTTPS包是明文的

    為什麼用抓包工具看HTTPS包是明文的

    測試或者開發調試的過程中,經常會進行抓包分析,並且裝上抓包工具的證書就能抓取 HTTPS 的數據包並显示。由此就產生了一個疑問,為什麼抓包工具裝上證書後就能抓到 HTTPS 的包並显示呢?不是說 HTTPS 是加密傳輸的嗎?

    今天這篇文章就來探究下上面這個問題,要解釋清楚這個問題,我會通過解答以下兩個問題來講述:

    1. HTTPS 到底是什麼?
    2. 抓包工具抓包的原理?

    HTTPS 到底是什麼

    HTTP 作為一種被廣泛使用的傳輸協議,也存在一些的缺點:

    1. 無狀態(可以通過 Cookie 或 Session 解決);
    2. 明文傳輸;
    3. 不安全;

    為了解決 “明文” 和 “不安全” 兩個問題,就產生了 HTTPSHTTPS 不是一種單獨的協議,它是由 HTTP + SSL/TLS 組成。

    HTTP與HTTPS

    所以要理解 HTTPS 就只需在 HTTP 的基礎上理解 SSL/TLS (TLS 是 SSL 的後續版本,現在一般使用 TLS),下面就來了解下 TLS 是什麼。

    TLS

    傳輸層安全性協議(英語:Transport Layer Security,縮寫:TLS)及其前身安全套接層(英語:Secure Sockets Layer,縮寫:SSL)是一種安全協議,目的是為互聯網通信提供安全及數據完整性保障。

    TLS 由記錄協議、握手協議、警報協議、變更密碼規範協議、擴展協議等幾個子協議組成,綜合使用了對稱加密、非對稱加密、身份認證等許多密碼學前沿技術。

    • 記錄協議 規定
      TLS 收發數據的基本單位為:記錄。類似
      TCP 里的
      segment,所有其它子協議都需要通過記錄協議發出。
    • 警報協議 的職責是向對方發出警報信息,類似於
      HTTP 里的狀態碼。
    • 握手協議
      TLS 里最複雜的子協議,瀏覽器和服務器在握手過程中會協商
      TLS 版本號、隨機數、密碼套件等信息,然後交換證書和密鑰參數,最終雙方協商得到會話密鑰,用於後續的混合加密系統。
    • 變更密碼規範協議 用於告知對方,後續的數據都將使用加密傳輸。

    TLS 的握手過程:

    TLS握手過程

    握手過程抓包显示:

    TLS抓包
    TLS所傳輸的數據

    交換密鑰的過程為:

    1. 客戶端發起一個請求給服務器;
    2. 服務器生成一對非對稱的公鑰(
      pubkey)和私鑰(
      privatekey),然後把公鑰附加到一個
      CA数字證書 上返回給客戶端;
    3. 客戶端校驗該證書是否合法(通過瀏覽器內置的廠商根證書等手段校驗),然後從證書中提取出公鑰(
      pubkey);
    4. 客戶端生成一個隨機數(
      key),然後使用公鑰(
      pubkey)對這個隨機數進行加密后發送給服務器;
    5. 服務器利用私鑰(
      privatekey)對收到的隨機數密文進行解密得到
      key ;
    6. 後續客戶端和服務器傳輸數據使用該
      key 進行加密后再傳輸;

    抓包工具抓包的原理

    先來看看抓 HTTP 包的原理

    HTTP抓包過程

    1. 首先抓包工具會提供出代理服務,客戶端需要連接該代理;
    2. 客戶端發出
      HTTP 請求時,會經過抓包工具的代理,抓包工具將請求的原文進行展示;
    3. 抓包工具使用該原文將請求發送給服務器;
    4. 服務器返回結果給抓包工具,抓包工具將返回結果進行展示;
    5. 抓包工具將服務器返回的結果原樣返回給客戶端;

    抓包工具就相當於個透明的中間人,數據經過的時候它一隻手接到數據,然後另一隻手把數據傳出去。

    再來看看 HTTPS 的抓包

    HTTPS抓包過程

    這個時候抓包工具對客戶端來說相當於服務器,對服務器來說相當於客戶端。在這個傳輸過程中,客戶端會以為它就是目標服務器,服務器也會以為它就是請求發起的客戶端。

    1. 客戶端連接抓包工具提供的代理服務;
    2. 客戶端需要安裝抓包工具的根證書;
    3. 客戶端發出
      HTTPS 請求,抓包工具模擬服務器與客戶端進行
      TLS 握手交換密鑰等流程;
    4. 抓包工具發送一個
      HTTPS 請求給客戶端請求的目標服務器,並與目標服務器進行
      TLS 握手交換密鑰等流程;
    5. 客戶端使用與抓包工具協定好的密鑰加密數據后發送給抓包工具;
    6. 抓包工具使用與客戶端協定好的密鑰解密數據,並將結果進行展示;
    7. 抓包工具將解密后的客戶端數據,使用與服務器協定好的密鑰進行加密后發送給目標服務器;
    8. 服務器解密數據后,做對應的邏輯處理,然後將返回結果使用與抓包工具協定好的密鑰進行加密發送給抓包工具;
    9. 抓包工具將服務器返回的結果,用與服務器協定好的密鑰解密,並將結果進行展示;
    10. 抓包工具將解密后的服務器返回數據,使用與客戶端協定好的密鑰進行加密后發送給客戶端;
    11. 客戶端解密數據;

    總結

    • HTTPS 不是單獨的一個協議,它是
      HTTP +
      SSL/TLS 的組合;
    • TLS 是傳輸層安全性協議,它會對傳輸的
      HTTP 數據進行加密,使用非對稱加密和對稱加密的混合方式;
    • 抓包工具的原理就是“偽裝“,對客戶端偽裝成服務器,對服務器偽裝成客戶端;
    • 使用抓包工具抓
      HTTPS 包必須要將抓包工具的證書安裝到客戶端本地,並設置信任;
    • HTTPS 數據只是在傳輸時進行了加密,而抓包工具是接收到數據后再重新加密轉發,所以抓包工具抓到的
      HTTPS 包可以直接看到明文;

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

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

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

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

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

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

    ※回頭車貨運收費標準

  • Python 為什麼不支持 i++ 自增語法,不提供 ++ 操作符?

    Python 為什麼不支持 i++ 自增語法,不提供 ++ 操作符?

    在 C/C++/Java 等等語言中,整型變量的自增或自減操作是標配,它們又可分為前綴操作(++i 和 –i)與後綴操作(i++ 和 i–),彼此存在着一些細微差別,各有不同的用途。

    這些語言的使用者在接觸 Python 時,可能會疑惑為什麼它不提供 ++ 或 — 的操作呢?在我前不久發的《Python的十萬個為什麼?》里,就有不少同學在調查問卷中表示了對此話題感興趣。

    Python 中雖然可能出現 ++i 這種前綴形式的寫法,但是它並沒有“++”自增操作符,此處只是兩個“+”(正數符號)的疊加而已,至於後綴形式的“++”,則完全不支持(SyntaxError: invalid syntax)。

    本期“Python為什麼 ”欄目,我們將會從兩個主要的角度來回答:Python 為什麼不支持 i++ 自增語法? (PS:此處自增指代“自增和自減”,下同)

    首先,Python 當然可以實現自增效果,即寫成i += 1 或者 i = i + 1 ,這在其它語言中也是通用的。

    雖然 Python 在底層用了不同的魔術方法(__add__()__iadd__() )來完成計算,但表面上的效果完全相同。

    所以,我們的問題可以轉化成:為什麼上面的兩種寫法會勝過 i++,成為 Python 的最終選擇呢?

    1、Python 的整數是不可變類型

    當我們定義i = 1000 時,不同語言會作出不同的處理:

    • C 之類的語言(寫法 int i = 1000)會申請一塊內存空間,並給它“綁定”一個固定的名稱 i,同時寫入一個可變的值 1000。在這裏,i 的地址以及類型是固定的,而值是可變的(在一定的表示範圍內)
    • Python(寫法i = 1000)也會申請一塊內存空間,但是它會“綁定”給数字 1000,即這個 1000 的地址以及類型是固定的(immutable),至於 i,只是一個名稱標籤貼在 1000 上,自身沒有固定的地址和類型

    所以當我們令 i “自增”時(i = i + 1),它們的處理是不同的:

    • C 之類的語言先找到 i 的地址上存的數值,然後令它加 1,操作后新的數值就取代了舊的數值
    • Python 的操作過程是把 i 指向的数字加 1,然後把結果綁定到新申請的一塊內存空間,再把名稱標籤 i “貼”到新的数字上。新舊数字可以同時存在,不是取代關係

    打一個不太恰當的比方:C 中的 i 就像一個宿主,数字 1000 寄生在它上面;而 Python 中的 1000 像個宿主,名稱 i 寄生在它上面。C 中的 i 與 Python 中的 1000,它們則寄生在底層的內存空間上……

    還可以這樣理解:C 中的變量 i 是一等公民,数字 1000 是它的一個可變的屬性;Python 中的数字 1000 是一等公民,名稱 i 是它的一個可變的屬性。

    有了以上的鋪墊,我們再來看看 i++,不難發現:

    • C 之類的語言,i++ 可以表示 i 的数字屬性的增加,它不會開闢新的內存空間,也不會產生新的一等公民
    • Python 之類的語言,i++ 如果是對其名稱屬性的操作,那樣就沒有意義了(總不能按字母表順序,把 i 變成 j 吧);如果理解成對数字本體的操作,那麼情況就會變得複雜:它會產生新的一等公民 1001,因此需要給它分配一個內存地址,此時若佔用 1000 的地址,則涉及舊對象的回收,那原有對於 1000 的引用關係都會受到影響,所以只能開闢新的內存空間給 1001

    Python 若支持 i++,其操作過程要比 C 的 i++ 複雜,而且其含義也不再是“令数字增加1”(自增),而是“創建一個新的数字”(新增), 這樣的話,“自增操作符”(increment operator)就名不副實了。

    Python 在理論上可以實現 i++ 操作,但它就必須重新定義“自增操作符”,還會令有其它語言經驗的人產生誤解,不如就讓大家直接寫成i += 1 或者 i = i + 1 好了。

    2、Python 有可迭代對象

    C/C++ 等語言設計出 i++,最主要的目的是為了方便使用三段式的 for 結構:

    for(int i = 0; i < 100; i++){
        // 執行 xxx
    }
    

    這種程序關心的是数字本身的自增過程,数字做加法與程序體的執行相關聯。

    Python 中沒有這種 for 結構的寫法,它提供了更為優雅的方式:

    for i in range(100):
        # 執行 xxx
    
    my_list = ["你好", "我是Python貓", "歡迎關注"]
    for info in my_list:
        print(info)
    

    這裏體現了不同的思維方式,它關心的是在一個數值範圍內的迭代遍歷,並不關心也不需要人為對数字做加法。

    Python 中的可迭代對象/迭代器/生成器提供了非常良好的迭代/遍歷用法,能夠做到對 i++ 的完全替代。

    例如,上例中實現了對列表內值的遍歷,Python 還可以用 enumerate() 實現對下標與具體值的同時遍歷:

    my_list = ["你好", "我是Python貓", "歡迎關注"]
    for i, info in enumerate(my_list):
        print(i, info)
    
    # 打印結果:
    0 你好
    1 我是Python貓
    2 歡迎關注
    

    再例如對於字典的遍歷,Python 提供了 keys()、values()、items() 等遍歷方法,非常好用:

    my_dict = {'a': '1', 'b': '2', 'c': '3'}
    for key in my_dict.keys():
        print(key)
    
    for key, value in my_dict.items():
        print(key, value)
    

    有了這樣的利器,哪裡還有 i++ 的用武之地呢?

    不僅如此,Python 中基本上很少使用i += 1 或者 i = i + 1 ,由於存在着隨處可見的可迭代對象,開發者們很容易實現對一個數值區間的操作,也就很少有對於某個數值作累加的訴求了。

    所以,回到我們開頭的問題,其實這兩種“自增”寫法並沒有勝出 i++ 多少,只因為它們是通用型操作,又不需要引入新的操作符,所以 Python 才延續了一種基礎性的支持。真正的贏家其實是各種各樣的可迭代對象!

    稍微小結下:Python 不支持自增操作符,一方面是因為它的整數是不可變類型的一等公民,自增操作(++)若要支持,則會帶來歧義;另一方面主要因為它有更合適的實現,即可迭代對象,對遍歷操作有很好的支持。

    如果你覺得本文分析得不錯,那你應該會喜歡這些文章:

    1、Python為什麼使用縮進來劃分代碼塊?

    2、Python 的縮進是不是反人類的設計?

    3、Python 為什麼不用分號作語句終止符?

    4、Python 為什麼沒有 main 函數?為什麼我不推薦寫 main 函數?

    5、Python 為什麼推薦蛇形命名法?

    寫在最後:本文屬於“Python為什麼”系列(Python貓出品),該系列主要關注 Python 的語法、設計和發展等話題,以一個個“為什麼”式的問題為切入點,試着展現 Python 的迷人魅力。部分話題會推出視頻版,請在 B 站收看,觀看地址:視頻地址

    公眾號【Python貓】, 本號連載優質的系列文章,有Python為什麼系列、喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫作、優質英文推薦與翻譯等等,歡迎關注哦。

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

    【其他文章推薦】

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

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

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

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

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

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

  • 手把手教你學Numpy,搞定數據處理——收官篇

    手把手教你學Numpy,搞定數據處理——收官篇

    本文始發於個人公眾號:TechFlow,原創不易,求個關注

    今天是Numpy專題第6篇文章,我們一起來看看Numpy庫當中剩餘的部分。

    數組的持久化

    在我們做機器學習模型的研究或者是學習的時候,在完成了訓練之後,有時候會希望能夠將相應的參數保存下來。否則的話,如果是在Notebook當中,當Notebook關閉的時候,這些值就丟失了。一般的解決方案是將我們需要的值或者是數組“持久化”,通常的做法是存儲在磁盤上。

    Python當中讀寫文件稍稍有些麻煩,我們還需要創建文件句柄,然後一行行寫入,寫入完成之後需要關閉句柄。即使是用with語句,也依然不夠簡便。針對這個問題,numpy當中自帶了寫入文件的api,我們直接調用即可。

    通過numpy當中save的文件是二進制格式的,所以我們是無法讀取其中內容的,即使強行打開也會是亂碼。

    以二進制的形式存儲數據避免了數據類型轉化的過程,尤其是numpy底層的數據是以C++實現的,如果使用Python的文件接口的話,勢必要先轉化成Python的格式,這會帶來大量開銷。既然可以存儲,自然也可以讀取,我們可以調用numpy的load函數將numpy文件讀取進來。

    要注意我們保存的時候沒有添加文件後綴,numpy會自動為我們添加後綴,但是讀取的時候必須要指定文件的全名,否則會numpy無法找到,會引發報錯。

    不僅如此,numpy還支持我們同時保存多個數組進入一個文件當中。

    我們使用savez來完成,在這個api當中我們傳入了a=arr,b=arr,其實是以類似字典的形式傳入的。在文件當中,numpy會將變量名和數組的值映射起來。這樣我們在讀入的時候,就可以通過變量名訪問到對應的值了。

    如果要存儲的數據非常大的話,我們還可以對數據進行壓縮,我們只需要更換savez成savez_compressed即可。

    線性代數

    Numpy除了科學計算之外,另外一大強大的功能就是支持矩陣運算,這也是它廣為流行並且在機器學習當中大受歡迎的原因之一。我們在之前的線性代數的文章當中曾經提到過Numpy這方面的一些應用,我們今天再在這篇文章當中匯總一些常用的線性代數的接口。

    點乘

    說起來矩陣點乘應該是最常用的線代api了,比如在神經網絡當中,如果拋開激活函數的話,一層神經元對於當前數據的影響,其實等價於特徵矩陣點乘了一個係數矩陣。再比如在邏輯回歸當中,我們計算樣本的加權和的時候,也是通過矩陣點乘來實現的。

    在Andrew的深度學習課上,他曾經做過這樣的實現,對於兩個巨大的矩陣進行矩陣相乘的運算。一次是通過Python的循環來實現,一次是通過Numpy的dot函數實現,兩者的時間開銷相差了足足上百倍。這當中的效率差距和Python語言的特性以及併發能力有關,所以在機器學習領域當中,我們總是將樣本向量化或者矩陣化,通過點乘來計算加權求和,或者是係數相乘。

    在Numpy當中我們採用dot函數來計算兩個矩陣的點積,既可以寫成a.dot(b),也可以寫成np.dot(a, b)。一般來說我更加喜歡前者,因為寫起來更加方便清晰。如果你喜歡後者也問題不大,這個只是個人喜好。

    注意不要寫成*,這個符號代表兩個矩陣元素兩兩相乘,而不是進行點積運算。它等價於np當中的multiply函數。

    轉置與逆矩陣

    轉置我們曾經在之前的文章當中提到過,可以通過.T或者是np.transpose來完成。

    Numpy中還提供了求解逆矩陣的操作,這個函數在numpy的linalg路徑下,這個路徑下實現了許多常用的線性代數函數。根據線性代數當中的知識,只有滿秩的方陣才有逆矩陣。我們可以通過numpy.linalg.det先來計算行列式來判斷,否則如果直接調用的話,對於沒有逆矩陣的矩陣會報錯。

    在這個例子當中,由於矩陣b的行列式為0,說明它並不是滿秩的,所以我們求它的逆矩陣會報錯。

    除了這些函數之外,linalg當中還封裝了其他一些常用的函數。比如進行qr分解的qr函數,進行奇異值分解的svd函數,求解線性方程組的solve函數等。相比之下,這些函數的使用頻率相對不高,所以就不展開一一介紹了,我們可以用到的時候再去詳細研究。

    隨機

    Numpy當中另外一個常用的領域就是隨機數,我們經常使用Numpy來生成各種各樣的隨機數。這一塊在Numpy當中其實也有很多的api以及很複雜的用法,同樣,我們不過多深入,挑其中比較重要也是經常使用的和大家分享一下。

    隨機數的所有函數都在numpy.random這個路徑下,我們為了簡化,就不寫完整的路徑了,大家記住就好。

    randn

    這個函數我們經常在代碼當中看到,尤其是我們造數據的時候。它代表的是根據輸入的shape生成一批均值為0,標準差為1的正態分佈的隨機數。

    要注意的是,我們傳入的shape不是一個元組,而是每一維的大小,這一點和其他地方的用法不太一樣,需要注意一下。除了正態分佈的randn之外,還有均勻分佈的uniform和Gamma分佈的gamma,卡方分佈的chisquare。

    normal

    normal其實也是生成正態分佈的樣本值,但不同的是,它支持我們指定樣本的均值和標準差。如果我們想要生成多個樣本,還可以在size參數當中傳入指定的shape。

    randint

    顧名思義,這個函數是用來生成隨機整數的。它接受傳入隨機數的上下界,最少也要傳入一個上界(默認下界是0)。

    如果想要生成多個int,我們可以在size參數傳入一個shape,它會返回一個對應大小的數組,這一點和uniform用法一樣。

    shuffle

    shuffle的功能是對一個數組進行亂序,返回亂序之後的結果。一般用在機器學習當中,如果存在樣本聚集的情況,我們一般會使用shuffle進行亂序,避免模型受到樣本分佈的影響。

    shuffle是一個inplace的方法,它會在原本值上進行改動,而不會返回一個新值。

    choice

    這也是一個非常常用的api,它可以在數據當中抽取指定條數據。

    但是它只支持一維的數組,一般用在批量訓練的時候,我們通過choice採樣出樣本的下標,再通過數組索引去找到這些樣本的值。比如這樣:

    總結

    今天我們一起研究了Numpy中數據持久化、線性代數、隨機數相關api的使用方法,由於篇幅的限制,我們只是選擇了其中比較常用,或者是比較重要的用法,還存在一些較為冷門的api和用法,大家感興趣的可以自行研究一下,一般來說文章當中提到的用法已經足夠了。

    今天這篇是Numpy專題的最後一篇了,如果你堅持看完本專題所有的文章,那麼相信你對於Numpy包一定有了一個深入的理解和認識了,給自己鼓鼓掌吧。之後周四會開啟Pandas專題,敬請期待哦。

    如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

    本文使用 mdnice 排版

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

    【其他文章推薦】

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

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

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

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

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

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • JAVA設計模式 2【創建型】原型模式的理解與使用、理解淺克隆和深克隆

    JAVA設計模式 2【創建型】原型模式的理解與使用、理解淺克隆和深克隆

    在本節中,我們將學習和使用原型模式;這一節學習的原型模式也是創建型 模式的其中之一。再次複習一下:創建型 模式就是描述如何去更好的創建一個對象。

    我們都知道,在JAVA 語言中。使用new 關鍵字創建一個新對象。將新的對象放到堆內存 裏面。當然,這個內存肯定是有大小限制的,況且,JAVA 不同於C語言等。 有內存管理機制,就是我們常說的垃圾回收器GC,才可以保證內存不被溢出。

    說這些其實就是為了表示:為啥要用單例模式,能節省內存的時候,能用一個對象解決重複的事情,絕對不會創建多個。

    概述

    原型模式描述的如何快速創建重複的對象,並且減少new 關鍵字的使用。

    • 抽象原型類
    • 具體原型類
    • 訪問類

    容我來一個一個解釋:

    抽象原型類 也就是我們具體要實現的某個類,這個類在JAVA 裏面是有具體的接口的,其實是一個空接口,Cloneable

     * @author  unascribed
     * @see     java.lang.CloneNotSupportedException
     * @see     java.lang.Object#clone()
     * @since   JDK1.0
     */
    public interface Cloneable {
    }
    

    我們會發現,這個類沒有任何的方法,怎麼來實現它,不要慌。先接着走。

    具體原型類 也就是我們具體要克隆 的對象。比如我們重複的要創建100個學生Student 對象,那麼具體的學生對象就是具體原型類

    public class Student implements Cloneable {
    
        private int id;
    
        private String name;
    
        private int sex;
    }
    

    訪問類 我就不必多說了

    淺克隆和深克隆

    原型模式其實也分淺克隆和深克隆。如何理解這兩個概念呢?

    淺克隆

    protected native Object clone() throws CloneNotSupportedException;
    

    淺克隆,只需要具體原型類 實現Cloneable 接口,並且重寫父類Object類的clone() 方法,即可實現對象的淺克隆。

    Student student1 = new Student(1, "李四");
    Student student2 = student1.clone();
    
    System.out.println(student1);
    System.out.println(student2);
    
    System.out.println(student1 == student2);
    ---------------------
    學號:1,姓名:李四
    學號:1,姓名:李四
    false
    
    • 通過執行clone() 方法即可創建一個相同的,具有同樣屬性的對象。
    • 並且是新的對象,內存地址有所不同。

    我們來看看,對於引用類型的變量,淺克隆是否可以進行克隆;

    Teacher teacher = new Teacher(1, "張老師");
    
    Student student1 = new Student(1, "李四", teacher);
    Student student2 = student1.clone();
    
    System.out.println(student1);
    System.out.println(student2);
    
    System.out.println(student1 == student2);
    ------------
    學號:1,姓名:李四,老師=Teacher@1b6d3586
    學號:1,姓名:李四,老師=Teacher@1b6d3586
    false
    

    我們發現,引用類型並沒有被克隆,也就是說:

    特點

    • 淺克隆對於基本類型,可以進行完全的克隆,並且克隆的對象是一個新的對象
    • 但是對象裏面的引用,是無法被克隆的。

    深克隆(序列化)

    何謂序列化?

    我們創建的都是保存在內存裏面的,只要被虛擬機GC進行回收,那麼這個對象的任何屬性都是消失,我們能不能找一個方法,將內存中這種對象的屬性以及對象的狀態通過某種東西保存下來,比如保存到數據庫,下次從數據庫將這個對象還原到內存裏面。 這就是序列化。

    • 序列化 內存對象->序列字符
    • 反序列化 序列字符->內存對象

    請參考: https://baike.baidu.com/item/序列化/2890184

    JAVA 序列化

     * @see java.io.Externalizable
     * @since   JDK1.1
     */
    public interface Serializable {
    }
    

    JAVA 提供了一個空接口,其實這個接口和上面的Cloneable 一樣,都是一個空接口,其實這個空接口就是作為一種標識 你的對象實現了這個接口,JAVA 認為你的這個就可以被序列化 ,就是這麼簡單。

    Teacher teacher = new Teacher(1, "張老師");
    
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    ObjectOutputStream stream = new ObjectOutputStream(outputStream);
    
    stream.writeObject(teacher);
    System.out.println(Arrays.toString(outputStream.toByteArray()));
    ----------
    [-84, -19, 0, 5, 115, 114, 0, 7, 84, 101, 97,。。。。。。
    

    通過將對象序列化、其實也就是將內存中的對象轉化為二進制 字節數組

    反序列化

    Teacher teacher = new Teacher(1, "張老師");
    System.out.println(teacher);
    
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    ObjectOutputStream stream = new ObjectOutputStream(outputStream);
    
    stream.writeObject(teacher);
    System.out.println(Arrays.toString(outputStream.toByteArray()));
    
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(outputStream.toByteArray());
    ObjectInputStream inputStream = new ObjectInputStream(byteArrayInputStream);
    
    Teacher teacher1 = (Teacher) inputStream.readObject();
    System.out.println(teacher1);
    ---------------
    id=1,name=張老師
    [-84, -19, 0, 5, 115, xxxxx,-127, -27, -72, -120]
    id=1,name=張老師
    

    通過序列化和反序列化,即可對象的深克隆

    小結

    這一節,在講述 原型模式的同時,將原有實現原型模式的clone() 淺克隆,延伸到深克隆這一概念。其實JAVA 的原型模式,實現起來較為簡單。但還是要按需要實現,Object 類提供的 clone 淺克隆 是沒辦法克隆對象的引用類型的。需要克隆引用類型,還是需要序列化 深克隆

    參考

    http://c.biancheng.net/view/1343.html
    https://www.liaoxuefeng.com/wiki/1252599548343744/1298366845681698

    代碼示例

    https://gitee.com/mrc1999/Dev-Examples

    歡迎關注

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

    【其他文章推薦】

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

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

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

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

    ※超省錢租車方案

    FB行銷專家,教你從零開始的技巧

  • Java多線程之內存模型

    Java多線程之內存模型

    目錄

    • 多線程需要解決的問題
      • 線程之間的通信
      • 線程之間的同步
    • Java內存模型
      • 內存間的交互操作
      • 指令屏障
      • happens-before規則
    • 指令重排序
      • 從源程序到字節指令的重排序
      • as-if-serial語義
      • 程序順序規則
    • 順序一致性模型
      • 順序一致性模型特性
      • 順序一致性模型特性
      • 當程序未正確同步會發生什麼
    • 參考資料

    多線程需要解決的問題

    在多線程編程中,線程之間如何通信和同步是一個必須解決的問題:

    線程之間的通信:

    線程之間有兩種通信的方式:消息傳遞和共享內存

    • 共享內存:線程之間共享程序的公共狀態,通過讀——寫修改公共狀態進行隱式通信。如上面代碼中的numLock可以被理解為公共狀態
    • 消息傳遞:線程之間沒有公共狀態,必須通過發送消息來進行显示通信
      在java中,線程是通過共享內存來完成線程之間的通信

    線程之間的同步:

    同步指程序中永固空值不同線程間的操作發生的相對順序的機制

    • 共享內存:同步是显示進行的,程序員需要指定某個方法或者某段代碼需要在線程之間互斥執行。如上面代碼中的Lock加鎖和解鎖之間的代碼塊,或者被synchronized包圍的代碼塊
    • 消息傳遞:同步是隱式執行的,因為消息的發送必然發生在消息的接收之前,例如使用Objetc#notify(),喚醒的線程接收信號一定在發送喚醒信號的發送之後。

    Java內存模型

    在java中,所有的實例域,靜態域、數組都被存儲在堆空間當中,堆內存在線程之間共享。

    所有的局部變量,方法定義參數和異常處理器參數不會被線程共享,在每個線程棧中獨享,他們不會存在可見性和線程安全問題。

    從Java線程模型(JMM)的角度來看,線程之間的共享變量存儲在主內存當中,每個線程擁有一個私有的本地內存(工作內存)本地內存存儲了該線程讀——寫共享的變量的副本。
    JMM只是一個抽象的概念,在現實中並不存在,其中所有的存儲區域都在堆內存當中。JMM的模型圖如下圖所示:

    而java線程對於共享變量的操作都是對於本地內存(工作內存)中的副本的操作,並沒有對共享內存中原始的共享變量進行操作;

    以線程1和線程2為例,假設線程1修改了共享變量,那麼他們之間需要通信就需要兩個步驟:

    1. 線程1本地內存中修改過的共享變量的副本同步到共享內存中去
    2. 線程2從共享內存中讀取被線程1更新過的共享變量
      這樣才能完成線程1的修改對線程2的可見。

    內存間的交互操作

    為了完成這一線程之間的通信,JMM為內存間的交互操作定義了8個原子操作,如下錶:

    操作 作用域 說明
    lock(鎖定) 共享內存中的變量 把一個變量標識為一條線程獨佔的狀態
    unlock(解鎖) 共享內存中的變量 把一個處於鎖定的變量釋放出來,釋放后其他線程可以進行訪問
    read(讀取) 共享內存中的變量 把一個變量的值從共享內存傳輸到線程的工作內存。供隨後的load操作使用
    load(載入) 工作內存 把read操作從共享內存中得到的變量值放入工作內存的變量副本當中
    use(使用) 工作內存 把工作內存中的一個變量值傳遞給執行引擎
    assign(賦值) 工作內存 把一個從執行引擎接受到的值賦值給工作內存的變量
    store(存儲) 作用於工作內存 把一個工作內存中的變量傳遞給共享內存,供後續的write使用
    write(寫入) 共享內存中的變量 把store操作從工作內存中得到的變量的值放入主內存

    JMM規定JVM四線時必須保證上述8個原子操作是不可再分割的,同時必須滿足以下的規則:

    1. 不允許readloadstorewrite操作之一單獨出現,即不允許只從共享內存讀取但工作內存不接受,或者工作捏村發起回寫但是共享內存不接收
    2. 不允許一個線程捨棄assign操作,即當一個線程修改了變量后必須寫回工作內存和共享內存
    3. 不允許一個線程將未修改的變量值寫回共享內存
    4. 變量只能從共享內存中誕生,不允許線程直接使用未初始化的變量
    5. 一個變量同一時刻只能由一個線程對其執行lock操作,但是一個變量可以被同一個線程重複執行多次lock,但是需要相同次數的unlock
    6. 如果對一個變量執行lock操作,那麼會清空工作內存中此變量的值,在執行引擎使用這個變量之前需要重新執行load和assign
    7. 不允許unlock一個沒有被鎖定的變量,也不允許unlock一個其他線程lock的變量
    8. 對一個變量unlock之前必須把此變量同步回主存當中。

    longdouble的特殊操作
    在一些32位的處理器上,如果要求對64位的longdouble的寫具有原子性,會有較大的開銷,為了照固這種情況,
    java語言規範鼓勵但不要求虛擬機對64位的longdouble型變量的寫操作具有原子性,當JVM在這種處理器上運行時,
    可能會把64位的long和double拆分成兩次32位的寫

    指令屏障

    為了保證內存的可見性,JMM的編譯器會禁止特定類型的編譯器重新排序;對於處理器的重新排序,
    JMM會要求編譯器在生成指令序列時插入特定類型的的內存屏障指令,通過內存屏障指令巾紙特定類型的處理器重新排序

    JMM規定了四種內存屏障,具體如下:

    屏障類型 指令示例 說明
    LoadLoad Barriers Load1;LoadLoad;Load2 確保Load1的數據先於Load2以及所有後續裝在指令的裝載
    StoreStore Barries Store1;StoreStore;Store2 確保Store1數據對於其他處理器可見(刷新到內存)先於Store2及後續存儲指令的存儲
    LoadStore Barriers Load1;LoadStore;Store2 確保Load1的裝載先於Store2及後續所有的存儲指令
    StoreLoad Barrier Store1;StoreLoad;Load2 確保Store1的存儲指令先於Load1以及後續所所有的加載指令

    StoreLoad是一個“萬能”的內存屏障,他同時具有其他三個內存屏障的效果,現代的處理器大都支持該屏障(其他的內存屏障不一定支持),
    但是執行這個內存屏障的開銷很昂貴,因為需要將處理器緩衝區所有的數據刷回內存中。

    happens-before規則

    在JSR-133種內存模型種引入了happens-before規則來闡述操作之間的內存可見性。在JVM種如果一個操作的結果過需要對另一個操作可見,
    那麼兩個操作之間必然要存在happens-bsfore關係:

    • 程序順序規則:一個線程中的個每個操作,happens-before於該線程的後續所有操作
    • 監視器鎖規則:對於一個鎖的解鎖,happens-before於隨後對於這個鎖的加鎖
    • volatitle變量規則:對於一個volatile的寫,happens-before於認意後續對這個volatile域的讀
    • 線程啟動原則:對線程的start()操作先行發生於線程內的任何操作
    • 線程終止原則:線程中的所有操作先行發生於檢測到線程終止,可以通過Thread.join()、Thread.isAlive()的返回值檢測線程是否已經終止
    • 線程終端原則:對線程的interrupt()的調用先行發生於線程的代碼中檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測是否發生中斷
    • 對象終結原則:一個對象的初始化完成(構造方法執行結束)先行發生於它的finalize()方法的開始。
    • 傳遞性:如果A happens-before B B happends-beforeC,那麼A happends-before C

    指令重排序

    從源程序到字節指令的重排序

    眾所周知,JVM執行的是字節碼,Java源代碼需要先編譯成字節碼程序才能在Java虛擬機中運行,但是考慮下面的程序;

    int a = 1;
    int b = 1;
    

    在這段代碼中,ab沒有任何的相互依賴關係,因此完全可以先對b初始化賦值,再對a變量初始化賦值;

    事實上,為了提高性能,編譯器和處理器通常會對指令做重新排序。重排序分為3種:

    1. 編譯器優化的重排序。編譯器在不改變單線程的程序語義的前提下,可以安排字語句的執行順序。編譯器的對象是語句,不是字節碼,
      但是反應的結果就是編譯后的字節碼和寫的語句順序不一致。
    2. 執行級并行的重排序。現代處理器採用了并行技術,來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
    3. 內存系統的重排序,由於處理器使用了緩存和讀/寫緩衝區,這使得加載和存儲操作看上去可能是在亂序執行。

    數據依賴性:如果兩個操作訪問同一個變量,且兩個操作有一個是寫操作,則這兩個操作存在數據依賴性,改變這兩個操作的執行順序,就會改變執行結果。

    儘管指令重排序會提高代碼的執行效率,但是卻為多線程編程帶來了問題,多線程操作共享變量需要一定程度上遵循代碼的編寫順序,
    也需要將修改的共享數據存儲到共享內存中,不按照代碼順序執行可能會導致多線程程序出現內存可見性的問題,那又如何實現呢?

    as-if-serial語義

    as-if-serial語義:不論程序怎樣進行重排序,(單線程)程序的執行結果不能被改變。編譯器、runtime和處理器都必須支持as-if-serial語義。

    程序順序規則

    假設存在以下happens-before程序規則:

        1) A happens-before B
        2) B happens-before C
        3) A happens-before C
    

    儘管這裏存在A happens-before B這一關係,但是JMM並不要求A一定要在B之前執行,僅僅要求A的執行結果對B可見。
    即JMM僅要求前一個操作的結果對於后一個操作可見,並且前一個操作按照順序排在後一個操作之前。
    但是若前一個操作放在後一個操作之後執行並不影響執行結果,則JMM認為這並不違法,JMM允許這種重排序。

    順序一致性模型

    在一個線程中寫一個變量,在另一個線程中同時讀取這個變量,讀和寫沒有通過排序來同步來排序,就會引發數據競爭。

    數據競爭的核心原因是程序未正確同步。如果一個多線程程序是正確同步的,這個程序將是一個沒有數據競爭的程序。

    順序一致性模型只是一個參考模型。

    順序一致性模型特性

    • 一個線程中所有的操作必須按照程序的順序來執行。
    • 不管線程是否同步,所有的線程都只能看到一個單一的執行順序。

    在順序一致性模型中每個曹祖都必須原子執行且立刻對所有線程可見。

    當程序未正確同步會發生什麼

    當線程未正確同步時,JMM只提供最小的安全性,當讀取到一個值時,這個值要麼是之前寫入的值,要麼是默認值。
    JMM保證線程的操作不會無中生有。為了保證這一特點,JMM在分配對象時,首先會對內存空間清0,然後才在上面分配對象。

    未同步的程序在JMM種執行時,整體上是無序的,執行結果也無法預知。位同步程序子兩個模型中執行特點有如下幾個差異:

    • 順序一致性模型保證單線程內的操作會按照程序的順序執行,而JMM不保證單線程內的操作會按照程序的順序執行
    • 順序一致性模型保證所有線程只能看到一致的操作執行順序,而JMM不保證所有線程能看到一致的操作執行順序
    • JMM不保證對64位的longdouble型變量具有寫操作的原子性,而順序一致性模型保證對所有的內存的讀/寫操作都具有原子性

    參考資料

    java併發編程的藝術-方騰飛,魏鵬,程曉明著

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

    【其他文章推薦】

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

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

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

    ※超省錢租車方案

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

    網頁設計最專業,超強功能平台可客製化

    【其他文章推薦】

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

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

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

    ※超省錢租車方案

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

    網頁設計最專業,超強功能平台可客製化

  • Windows7/10實現ICMP(ping命令)

    Windows7/10實現ICMP(ping命令)

      如果覺得本文如果幫到你或者你想轉載都可以,只需要標註出處即可。謝謝

     利用ICMP數據包、C語言實現Ping命令程序,能實現基本的Ping操作,發送ICMP回顯請求報文,用於測試—個主機到只一個主機之間的連通情況。通過本程序的訓練,熟悉ICMP報文結構,對ICMP有更深的理解,掌握Ping程序的設計方法,掌握網絡編程的方法和技巧,從而編寫出功能更強大的程序。有關traceroute如果有時間我會也寫一篇來進行講解.W

      windows和Linux實現ping的底層思想一樣的,代碼有細微的差別。如文文件不一樣,參數定義不一樣等。所以我們要實現ping功能的時候我們需要注意是在Windows上實現還是Linux上實現。

      如果你不想看關於ping命令實現的原理,則可以直接通過以下目錄跳轉到‘8.實現Ping功能’即可.

      本文目錄

      1.ICMP簡介

      2.ICMP工作原理

      3.ICMP報文格式

      4.ICMPv4類型

        4.1響應請求/應答(ping)

        4.2.目標不可到達、源抑制和超時報文

      5.ICMP應用

      6.ICMP攻擊與防禦方法

      7.IP報文頭和ICMP的聯繫

      8.實現Ping功能

        8.1.ping實現步驟

        8.2.結果及心得

        8.3.完整代碼

    1.ICMP簡介

      ICMP(Internet Control Message Protocol)Internet控制報文協議。它是TCP/IP協議簇的一個子協議,用於在IP主機、路由器之間傳遞控制消息。控制消息是指網絡通不通、主機是否可達、路由是否可用等網絡本身的消息。這些控制消息雖然並不傳輸用戶數據,但是對於用戶數據的傳遞起着重要的作用。

     ICMP協議是一種面向無連接的協議,用於傳輸出錯報告控制信息。它是一個非常重要的協議,它對於網絡安全具有極其重要的意義。

      ICMP報文通常是由IP層本身、上層的傳輸協議(TCP或UDP)甚至某些情況下用戶應用除法執行的。

      ICMP報文是在IP數據報內被封裝傳輸的。

      ICMP分為兩大類:有關IP數據報傳遞的ICMP報文(稱為差錯報文(error message)),以及有關信息採集和配置的ICMP報文(稱為查詢(query)或者信息類報文(informational message))。

      注:ICMP並不為IP網絡提供可靠性。相反,它表明了某些類別的故障和配置信息。

    2.ICMP工作原理

      ICMP提供一致易懂的出錯報告信息。發送的出錯報文返回到發送原數據的設備,因為只有發送設備才是出錯報文的邏輯接受者。發送設備隨後可根據ICMP報文確定發生錯誤的類型,並確定如何才能更好地重發失敗的數據包。但是ICMP唯一的功能是報告問題而不是糾正錯誤,糾正錯誤的任務由發送方完成。

      我們在網絡中經常會使用到ICMP協議,比如我們經常使用的用於檢查網絡通不通的Ping命令(Linux和Windows中均有),這個“Ping”的過程實際上就是ICMP協議工作的過程。還有其他的網絡命令如跟蹤路由的Tracert命令也是基於ICMP協議的。

    3.ICMP報文格式

      ICMP報文包含在IP數據報中,屬於IP的一個用戶,IP頭部就在ICMP報文的前面,所以一個ICMP報文包括IP頭部、ICMP頭部和ICMP報文,IP頭部的Protocol值為1就說明這是一個ICMP報文,ICMP頭部中的類型(Type)域用於說明ICMP報文的作用及格式,此外還有一個代碼(Code)域用於詳細說明某種ICMP報文的類型,所有數據都在ICMP頭部後面。

      ICMPICMP報文格式具體由[RFC777][RFC792]規範。792是1981年9月更新,而777是1981年4月更新的。目前最新的ICMP報文格式RFC是2007年4月更新的[RFC488].

     

    4.ICMPv4類型

      已經定義的ICMP消息類型大約有10多種,每種ICMP數據類型都被封裝在一個IP數據包中。主要的ICMP消息類型包括以下幾種。

      對於ICMPv4,信息類報文包括回顯請求和回顯應答(分別為類型8和0),以及路由器通告和路由器請求(分別為類型9和10,統一被稱為路由器發現)。最常見的差錯報文類型包括目的不可達(類型3)、重定向(類型5)、超時(類型11)和參數問題(類型12).下圖為一些類型.更多的信息建議去RFC官方查看,Type和Code在IPv4和IPc6不盡相同,所以其中的差異需要我們自行去查看,本圖為IPv4版本的,IPv6需要我們自己RFC查找。

     

    1).響應請求/應答(ping)(ICMPv4類型為0/8,ICMPv6類型129/18)

      我們日常使用最多的ping,就是響應請求(Type=8)和應答(Type=0),一台主機向一個節點發送一個Type=8的ICMP報文,如果途中沒有異常(例如被路由器丟棄、目標不回應ICMP或傳輸失敗),則目標返回Type=0的ICMP報文,說明這台主機存在,更詳細的tracert通過計算ICMP報文通過的節點來確定主機與目標之間的網絡距離。更多的信息我們可以通過RFC文檔了解

     

    2).目標不可到達(ICMPv4類型3,ICMPv6類型1)、源抑制和超時報文(ICMPv4類型11,ICMPv6類型4)

      這三種報文的格式是一樣的,目標不可到達報文(Type=3)在路由器或主機不能傳遞數據報時使用,例如我們要連接對方一個不存在的系統端口(端口號小於1024)時,將返回Type=3、Code=3的ICMP報文,它要告訴我們:“嘿,別連接了,我不在家的!”,常見的不可到達類型還有網絡不可到達(Code=0)、主機不可到達(Code=1)、協議不可到達(Code=2)等。源抑制則充當一個控制流量的角色,它通知主機減少數據報流量,由於ICMP沒有恢復傳輸的報文,所以只要停止該報文,主機就會逐漸恢復傳輸速率。最後,無連接方式網絡的問題就是數據報會丟失,或者長時間在網絡遊盪而找不到目標,或者擁塞導致主機在規定時間內無法重組數據報分段,這時就要觸發ICMP超時報文的產生。超時報文的代碼域有兩種取值:Code=0表示傳輸超時,Code=1表示重組分段超時。更多的信息我們可以通過RFC文檔了解

     

    5.ICMP應用

    1).ping 命令使用 ICMP 回送請求和應答報文在網絡可達性測試中使用的分組網間探測命令 ping 能產生 ICMP 回送請求和應答報文。目的主機收到 ICMP 回送請求報文後立刻回送應答報文,若源主機能收到 ICMP 回送應答報文,則說明到達該主機的網絡正常。

    2).路由分析診斷程序 tracert 使用了 ICMP時間超過報文tracert 命令主要用來显示數據包到達目的主機所經過的路徑。通過執行一個 tracert 到對方主機的命令,返回數據包到達目的主機所經歷的路徑詳細信息,並显示每個路徑所消耗的時間。

     

    6.ICMP攻擊

      涉及ICMP的攻擊主要分為3類:泛洪(flood)、炸彈(bomb)、信息泄露(information disclosure).針對TCP的ICMP攻擊已經被專門記錄在RFC文檔中[RFC5927]

    1).泛洪(flood)

      泛洪將會生成大量流量,導致針對一台或者多台計算機的有效Dos攻擊

    2).炸彈(bomb)

      炸彈類型有時也稱為核彈(nuke)類型,指的是發送經過特殊構造的報文,能夠導致IP或ICMP的處理崩潰或者終止。

    3).信息泄露(information disclosure)

      信息泄露攻擊本身不會造成危害,但是能夠幫助其他攻擊方法避免浪費時間或者被發現了。

    7.IP報文頭和ICMP的聯繫

      ICMP報文是封裝在IP數據報的數據部分中進行傳輸的.

     

      ICMP依靠IP來完成它的任務,它是IP的主要部分。它與傳輸協議(如TCP和UDP)顯著不同:它一般不用於在兩點間傳輸數據。它通常不由網絡程序直接使用,除了 ping 和 traceroute 這兩個特別的例子。 IPv4中的ICMP被稱作ICMPv4,IPv6中的ICMP則被稱作ICMPv6。

     

      總的來說,ICMP是封裝在IP數據報中進行傳輸的.具體更多的聯繫我們通過以下改文章進行詳解,從Wireshark抓包然後分析數據包進行兩者的區別和聯繫.

      參考文檔:https://www.cnblogs.com/CSAH/p/13170860.html

    8.實現Ping功能

      首先我們注意,本文只是實現ping的最簡單的功能即響應請求/應答(ping),故只能夠ping IP地址,不能夠ping 域名,因為域名到IP地址我們需要經過DNS解析,本文不實現該功能.關於DNS轉換到IP地址的詳情,有時間有機會我會補上的.

      本程序使用的環境是win10+vc++6.0,如果沒有安裝VC++6.0的或者在Win10安裝了無法使用的請查看’Win10安裝vc6.0教程‘。

      ping功能實現參考了TCP/IP詳解 卷1 和 卷2。

    1).實現步驟

      首先,我們需要先定義初始化一些全局變量,接着我們對需要用到的數據類型結構進行聲明定義,我們包含的數據類型結構有IP報頭結構、ICMP數據類型結構、結果集類型結構等;對需要使用到的函數進行頭文件的導入,主要的區別在於使用的是Windows系統還是Linux系統,導入的頭文件也不盡相同。準備工作全都完成了,然後我們就可以定義main函數進行試驗的驗證測試。

      其次,我們需要對每一步的遇到的問題需要寫一份說明報告書,以防下次再進行實驗時遇到同樣的問題時,我們無需再去查找大量資料。

      最後,我們對整個實驗的總結,對每一步。每一個函數進行詳講.做好註釋.

      Ping()函數是本程序的核心部分,它基本是調用其他模塊的函數來實現最終功能,其主要布驟包括:定義及初始化各個全局變量、打開socket動態庫、設置接收和發送超時值、域名地址解析、分配內存、創建及初始化ICMP報文、發送ICMP請求報文、接收ICMP 應答報文以及解讀應答報文和輸出Ping結果。

     

      注意:創建套接字的時候參數的以及在創建套接字之前必須首先使用WSAStartup函數。

    (1)輸入時不能輸入目標主機名,不然ping結果為TIMEOUT

     

    (2)該模塊並非只有處理還包括判斷及輸出判斷結果的含義

    (3)程序沒運行一次就只能輸出四行結果(前提是輸入的地址有效),欲再次PING其他地址接着輸入下一個ip地址即可

     

    2).代碼實現

        如果要想實現Windows下ping功能的實現,我們只需要從(1)到(8)複製到任意一個新創建filename.cpp文件中即可執行.或者最簡單的方法就是到本文中最低直接複製’完整代碼’到任意一個新創建filename.cpp文件中即可執行

    (1).頭文件、全局變量

    #include<stdio.h>
    #include<Winsock2.h>
    #include<ws2tcpip.h>
    #include<stdlib.h>
    #include<malloc.h>
    #include<string.h>
    #pragma comment(lib , "Ws2_32.lib")
    
    #define ICMP_ECHO_REQUEST 8 //定義回顯請求類型
    #define DEF_ICMP_DATA_SIZE 20 //定義發送數據長度
    #define DEF_ICMP_PACK_SIZE 32 //定義數據包長度
    #define MAX_ICMP_PACKET_SIZE 1024 //定義最大數據包長度
    #define DEF_ICMP_TIMEOUT 3000  //定義超時為3秒
    #define ICMP_TIMEOUT 11 //ICMP超時報文
    #define ICMP_ECHO_REPLY 0 //定義回顯應答類型
    

      

    (2).IP報頭據類型

    /*
     *IP報頭結構
     */
    typedef struct
    {
        byte h_len_ver ; //IP版本號
        byte tos ; // 服務類型
        unsigned short total_len ; //IP包總長度
        unsigned short ident ; // 標識
        unsigned short frag_and_flags ; //標誌位
        byte ttl ; //生存時間
        byte proto ; //協議
        unsigned short cksum ; //IP首部校驗和
        unsigned long sourceIP ; //源IP地址
        unsigned long destIP ; //目的IP地址
    } IP_HEADER ;
    

      

    (3).ICMP數據類型

    /*
     *定義ICMP數據類型
     */
    typedef struct _ICMP_HEADER
    {
        byte type ; //類型-----8
        byte code ; //代碼-----8
        unsigned short cksum ; //校驗和------16
        unsigned short id ; //標識符-------16
        unsigned short seq ; //序列號------16
        unsigned int choose ; //選項-------32
    } ICMP_HEADER ;
    

      

    (4).ping返回結果集數據類型

    typedef struct
    {
        int usSeqNo ; //記錄序列號
        DWORD dwRoundTripTime ; //記錄當前時間
        byte ttl ; //生存時間
        in_addr dwIPaddr ; //源IP地址
    } DECODE_RESULT ;
    

      

    (5).網際校驗和

    /*
     *產生網際校驗和
     */
    unsigned short GenerateChecksum(unsigned short *pBuf , int iSize)
    {
        unsigned long cksum = 0 ; //開始時將網際校驗和初始化為0
        while(iSize > 1)
        {
            cksum += *pBuf++ ; //將待校驗的數據每16位逐位相加保存在cksum中
            iSize -= sizeof(unsigned short) ; //每16位加完則將帶校驗數據量減去16
        }
        //如果待校驗的數據為奇數,則循環完之後需將最後一個字節的內容與之前結果相加
        if(iSize)
        {
            cksum += *(unsigned char*)pBuf ;
        }
            //之前的結果產生了進位,需要把進位也加入最後的結果中
        cksum = (cksum >> 16) + (cksum & 0xffff) ;
        cksum += (cksum >> 16) ;
        return (unsigned short)(~ cksum) ;
    }
    

      

    (6).ping信息解析

    /*
     *對ping應答信息進行解析
     */
    boolean DecodeIcmpResponse_Ping(char *pBuf , int iPacketSize , DECODE_RESULT *stDecodeResult)
    {
        IP_HEADER *pIpHrd = (IP_HEADER*)pBuf ;
        int iIphedLen = 20 ;
        if(iPacketSize < (int)(iIphedLen + sizeof(ICMP_HEADER)))
        {
            printf("size error! \n") ;
            return 0 ;
        }
        //指針指向ICMP報文的首地址
        ICMP_HEADER *pIcmpHrd = (ICMP_HEADER*)(pBuf + iIphedLen) ;
        unsigned short usID , usSeqNo ;
        //獲得的數據包的type字段為ICMP_ECHO_REPLY,即收到一個回顯應答ICMP報文
        if(pIcmpHrd->type == ICMP_ECHO_REPLY)
        {
            usID = pIcmpHrd->id ;
            //接收到的是網絡字節順序的seq字段信息 , 需轉化為主機字節順序
            usSeqNo = ntohs(pIcmpHrd->seq) ;
        }
        if(usID != GetCurrentProcessId() || usSeqNo != stDecodeResult->usSeqNo)
        {
            printf("usID error!\n") ;
            return 0 ;
        }
        //記錄對方主機的IP地址以及計算往返的時延RTT
        if(pIcmpHrd->type == ICMP_ECHO_REPLY)
        {
            stDecodeResult->dwIPaddr.s_addr = pIpHrd->sourceIP ;
            stDecodeResult->ttl = pIpHrd->ttl ;
            stDecodeResult->dwRoundTripTime = GetTickCount() - stDecodeResult->dwRoundTripTime ;
            return 1 ;
        }
        return 0 ;
    }
    

      

    (7).ping功能實現集成

    void Ping(char *IP)
    {
       unsigned long ulDestIP = inet_addr(IP) ; //將IP地址轉化為長整形
       if(ulDestIP == INADDR_NONE)
       {
           //轉化不成功時按域名解析
           HOSTENT *pHostent = gethostbyname(IP) ;
           if(pHostent)
           {
               ulDestIP = (*(IN_ADDR*)pHostent->h_addr).s_addr ; //將HOSTENT轉化為長整形
           }
           else
           {
               printf("TIMEOUT\n") ;
               return ;
           }
       }
       //填充目的Socket地址
       SOCKADDR_IN destSockAddr ; //定義目的地址
       ZeroMemory(&destSockAddr , sizeof(SOCKADDR_IN)) ; //將目的地址清空
       destSockAddr.sin_family = AF_INET ;
       destSockAddr.sin_addr.s_addr = ulDestIP ;
       destSockAddr.sin_port = htons(0);
        //初始化WinSock
        WORD wVersionRequested = MAKEWORD(2,2);
        WSADATA wsaData;
        if(WSAStartup(wVersionRequested,&wsaData) != 0)
        {
            printf("初始化WinSock失敗!\n") ;
            return ;
        }
       //使用ICMP協議創建Raw Socket
       SOCKET sockRaw = WSASocket(AF_INET , SOCK_RAW , IPPROTO_ICMP , NULL , 0 , WSA_FLAG_OVERLAPPED) ;
       if(sockRaw == INVALID_SOCKET)
       {
           printf("創建Socket失敗 !\n") ;
           return ;
       }
       //設置端口屬性
       int iTimeout = DEF_ICMP_TIMEOUT ;
       if(setsockopt(sockRaw , SOL_SOCKET , SO_RCVTIMEO , (char*)&iTimeout , sizeof(iTimeout)) == SOCKET_ERROR)
       {
             printf("設置參數失敗!\n") ;
             return ;
       }
       if(setsockopt(sockRaw , SOL_SOCKET , SO_SNDTIMEO , (char*)&iTimeout , sizeof(iTimeout)) == SOCKET_ERROR)
       {
             printf("設置參數失敗!\n") ;
             return ;
       }
       //定義發送的數據段
       char IcmpSendBuf[DEF_ICMP_PACK_SIZE] ;
       //填充ICMP數據包個各字段
       ICMP_HEADER *pIcmpHeader  = (ICMP_HEADER*)IcmpSendBuf;
       pIcmpHeader->type = ICMP_ECHO_REQUEST ;
       pIcmpHeader->code = 0 ;
       pIcmpHeader->id = (unsigned short)GetCurrentProcessId() ;
       memset(IcmpSendBuf + sizeof(ICMP_HEADER) , 'E' , DEF_ICMP_DATA_SIZE) ;
       //循環發送四個請求回顯icmp數據包
       int usSeqNo = 0 ;
       DECODE_RESULT stDecodeResult ;
       while(usSeqNo <= 3)
       {
         pIcmpHeader->seq = htons(usSeqNo) ;
         pIcmpHeader->cksum = 0 ;
         pIcmpHeader->cksum = GenerateChecksum((unsigned short*)IcmpSendBuf , DEF_ICMP_PACK_SIZE) ; //生成校驗位
         //記錄序列號和當前時間
         stDecodeResult.usSeqNo = usSeqNo ;
         stDecodeResult.dwRoundTripTime = GetTickCount() ;
         //發送ICMP的EchoRequest數據包
         if(sendto(sockRaw , IcmpSendBuf , DEF_ICMP_PACK_SIZE , 0 , (SOCKADDR*)&destSockAddr , sizeof(destSockAddr)) == SOCKET_ERROR)
         {
            //如果目的主機不可達則直接退出
            if(WSAGetLastError() == WSAEHOSTUNREACH)
            {
                printf("目的主機不可達!\n") ;
                exit(0) ;
            }
         }
         SOCKADDR_IN from ;
         int iFromLen = sizeof(from) ;
         int iReadLen ;
         //定義接收的數據包
         char IcmpRecvBuf[MAX_ICMP_PACKET_SIZE] ;
         while(1)
         {
             iReadLen = recvfrom(sockRaw , IcmpRecvBuf , MAX_ICMP_PACKET_SIZE , 0 , (SOCKADDR*)&from , &iFromLen) ;
             if(iReadLen != SOCKET_ERROR)
             {
                 if(DecodeIcmpResponse_Ping(IcmpRecvBuf , sizeof(IcmpRecvBuf) , &stDecodeResult))
                 {
                    printf("來自 %s 的回復: 字節 = %d 時間 = %dms TTL = %d\n" , inet_ntoa(stDecodeResult.dwIPaddr) ,
                             iReadLen - 20,stDecodeResult.dwRoundTripTime ,stDecodeResult.ttl) ;
                 }
                 break ;
             }
             else if(WSAGetLastError() == WSAETIMEDOUT)
             {
                 printf("time out !  *****\n") ;
                 break ;
             }
             else
             {
                 printf("發生未知錯誤!\n") ;
                 break ;
             }
         }
         usSeqNo++ ;
       }
       //輸出屏幕信息
       printf("Ping complete...\n") ;
       closesocket(sockRaw) ;
       WSACleanup() ;
    }
    

      

    ①.inet_addr:可以轉化字符串,主要用來將一個十進制的數轉化為二進制的數,用途多於ipv4的IP轉化。

    ②.if(IpAddress == INADDR_NONE):INADDR_NONE 是個宏定義,代表IpAddress是否為無效的IP地址。

    ③.ckaddr_in:定義目的地址信息;

     

    ④.ZeroMemory:用0來填充一塊內存區域.ZeroMemory只能用於windows平台.

     

    ⑤.WSASocket:創建一個原始套接字。使用時需要包含winsock2.h 頭文件和鏈接ws2_32.lib庫。

    ⑥.SOCKET socket==INVALID_SOCKET:如果socket為無效套接字,則結果為true;

    ⑦.DEF_ICMP_TIMEOUT:報文超時時間.

    ⑧.setsockopt:選項影響套接口的操作,諸如加急數據是否在普通數據流中接收,廣播數據是否可以從套接口發送等等。

    ⑨.while(usSeqNo <= 3){}:該部分就是實驗要求我們一次測試的進行發包4次

    (8).Test測試

    int main(int argc , char* argv[])
    {
       char  com[10] , IP[20] ;
       while(1){
       printf("command>>") ;
       scanf("%s %s" , com , IP) ;
       if(strcmp(com , "ping") == 0)
       {
           Ping(IP) ;
       }
       else
       {
           printf("輸入錯誤 ! \n") ;
       }
       }
       return 0 ;
    }
    

      

    2).結果及心得

    (1).查看本機IP

     

    (2).ping網關IP

     

    (3).ping本機IP

     

    (4).ping局域網內IP

     

    (5).問題與解決方案

    ①.問題:telnet是23端口,ssh是22端口,那麼ping是什麼端口?

    答:ping基於ICMP,是在網絡層運行的。而端口號為傳輸層的內容。所以在ICMP中根本就不需要關注端口號這樣的信息。

    ②.Win7、win10 在VC6.0運行時WSASocket 返回錯誤 10013

      

    3).完整代碼

     

    #include<stdio.h>
    #include<Winsock2.h>
    #include<ws2tcpip.h>
    #include<stdlib.h>
    #include<malloc.h>
    #include<string.h>
    #pragma comment(lib , "Ws2_32.lib")
    
    
    #define ICMP_ECHO_REQUEST 8 //定義回顯請求類型
    #define DEF_ICMP_DATA_SIZE 20 //定義發送數據長度
    #define DEF_ICMP_PACK_SIZE 32 //定義數據包長度
    #define MAX_ICMP_PACKET_SIZE 1024 //定義最大數據包長度
    #define DEF_ICMP_TIMEOUT 3000  //定義超時為3秒
    #define ICMP_TIMEOUT 11 //ICMP超時報文
    #define ICMP_ECHO_REPLY 0 //定義回顯應答類型
    /*
     *IP報頭結構
     */
    typedef struct
    {
        byte h_len_ver ; //IP版本號
        byte tos ; // 服務類型
        unsigned short total_len ; //IP包總長度
        unsigned short ident ; // 標識
        unsigned short frag_and_flags ; //標誌位
        byte ttl ; //生存時間
        byte proto ; //協議
        unsigned short cksum ; //IP首部校驗和
        unsigned long sourceIP ; //源IP地址
        unsigned long destIP ; //目的IP地址
    } IP_HEADER ;
    /*
     *定義ICMP數據類型
     */
    typedef struct _ICMP_HEADER
    {
        byte type ; //類型-----8
        byte code ; //代碼-----8
        unsigned short cksum ; //校驗和------16
        unsigned short id ; //標識符-------16
        unsigned short seq ; //序列號------16
        unsigned int choose ; //選項-------32
    } ICMP_HEADER ;
    
    
    typedef struct
    {
        int usSeqNo ; //記錄序列號
        DWORD dwRoundTripTime ; //記錄當前時間
        byte ttl ; //生存時間
        in_addr dwIPaddr ; //源IP地址
    } DECODE_RESULT ;
    
    /*
     *產生網際校驗和
     */
    unsigned short GenerateChecksum(unsigned short *pBuf , int iSize)
    {
        unsigned long cksum = 0 ; //開始時將網際校驗和初始化為0
        while(iSize > 1)
        {
            cksum += *pBuf++ ; //將待校驗的數據每16位逐位相加保存在cksum中
            iSize -= sizeof(unsigned short) ; //每16位加完則將帶校驗數據量減去16
        }
        //如果待校驗的數據為奇數,則循環完之後需將最後一個字節的內容與之前結果相加
        if(iSize)
        {
            cksum += *(unsigned char*)pBuf ;
        }
            //之前的結果產生了進位,需要把進位也加入最後的結果中
        cksum = (cksum >> 16) + (cksum & 0xffff) ;
        cksum += (cksum >> 16) ;
        return (unsigned short)(~ cksum) ;
    }
    
    /*
     *對ping應答信息進行解析
     */
    boolean DecodeIcmpResponse_Ping(char *pBuf , int iPacketSize , DECODE_RESULT *stDecodeResult)
    {
        IP_HEADER *pIpHrd = (IP_HEADER*)pBuf ;
        int iIphedLen = 20 ;
        if(iPacketSize < (int)(iIphedLen + sizeof(ICMP_HEADER)))
        {
            printf("size error! \n") ;
            return 0 ;
        }
        //指針指向ICMP報文的首地址
        ICMP_HEADER *pIcmpHrd = (ICMP_HEADER*)(pBuf + iIphedLen) ;
        unsigned short usID , usSeqNo ;
        //獲得的數據包的type字段為ICMP_ECHO_REPLY,即收到一個回顯應答ICMP報文
        if(pIcmpHrd->type == ICMP_ECHO_REPLY)
        {
            usID = pIcmpHrd->id ;
            //接收到的是網絡字節順序的seq字段信息 , 需轉化為主機字節順序
            usSeqNo = ntohs(pIcmpHrd->seq) ;
        }
        if(usID != GetCurrentProcessId() || usSeqNo != stDecodeResult->usSeqNo)
        {
            printf("usID error!\n") ;
            return 0 ;
        }
        //記錄對方主機的IP地址以及計算往返的時延RTT
        if(pIcmpHrd->type == ICMP_ECHO_REPLY)
        {
            stDecodeResult->dwIPaddr.s_addr = pIpHrd->sourceIP ;
            stDecodeResult->ttl = pIpHrd->ttl ;
            stDecodeResult->dwRoundTripTime = GetTickCount() - stDecodeResult->dwRoundTripTime ;
            return 1 ;
        }
        return 0 ;
    }
    
    void Ping(char *IP)
    {
       unsigned long ulDestIP = inet_addr(IP) ; //將IP地址轉化為長整形
       if(ulDestIP == INADDR_NONE)
       {
           //轉化不成功時按域名解析
           HOSTENT *pHostent = gethostbyname(IP) ;
           if(pHostent)
           {
               ulDestIP = (*(IN_ADDR*)pHostent->h_addr).s_addr ; //將HOSTENT轉化為長整形
           }
           else
           {
               printf("TIMEOUT\n") ;
               return ;
           }
       }
       //填充目的Socket地址
       SOCKADDR_IN destSockAddr ; //定義目的地址
       ZeroMemory(&destSockAddr , sizeof(SOCKADDR_IN)) ; //將目的地址清空
       destSockAddr.sin_family = AF_INET ;
       destSockAddr.sin_addr.s_addr = ulDestIP ;
       destSockAddr.sin_port = htons(0);
        //初始化WinSock
        WORD wVersionRequested = MAKEWORD(2,2);
        WSADATA wsaData;
        if(WSAStartup(wVersionRequested,&wsaData) != 0)
        {
            printf("初始化WinSock失敗!\n") ;
            return ;
        }
       //使用ICMP協議創建Raw Socket
       SOCKET sockRaw = WSASocket(AF_INET , SOCK_RAW , IPPROTO_ICMP , NULL , 0 , WSA_FLAG_OVERLAPPED) ;
       if(sockRaw == INVALID_SOCKET)
       {
           printf("創建Socket失敗 !\n") ;
           return ;
       }
       //設置端口屬性
       int iTimeout = DEF_ICMP_TIMEOUT ;
       if(setsockopt(sockRaw , SOL_SOCKET , SO_RCVTIMEO , (char*)&iTimeout , sizeof(iTimeout)) == SOCKET_ERROR)
       {
             printf("設置參數失敗!\n") ;
             return ;
       }
       if(setsockopt(sockRaw , SOL_SOCKET , SO_SNDTIMEO , (char*)&iTimeout , sizeof(iTimeout)) == SOCKET_ERROR)
       {
             printf("設置參數失敗!\n") ;
             return ;
       }
       //定義發送的數據段
       char IcmpSendBuf[DEF_ICMP_PACK_SIZE] ;
       //填充ICMP數據包個各字段
       ICMP_HEADER *pIcmpHeader  = (ICMP_HEADER*)IcmpSendBuf;
       pIcmpHeader->type = ICMP_ECHO_REQUEST ;
       pIcmpHeader->code = 0 ;
       pIcmpHeader->id = (unsigned short)GetCurrentProcessId() ;
       memset(IcmpSendBuf + sizeof(ICMP_HEADER) , 'E' , DEF_ICMP_DATA_SIZE) ;
       //循環發送四個請求回顯icmp數據包
       int usSeqNo = 0 ;
       DECODE_RESULT stDecodeResult ;
    
       while(usSeqNo <= 3)
       {
         pIcmpHeader->seq = htons(usSeqNo) ;
         pIcmpHeader->cksum = 0 ;
         pIcmpHeader->cksum = GenerateChecksum((unsigned short*)IcmpSendBuf , DEF_ICMP_PACK_SIZE) ; //生成校驗位
         //記錄序列號和當前時間
         stDecodeResult.usSeqNo = usSeqNo ;
         stDecodeResult.dwRoundTripTime = GetTickCount() ;
         //發送ICMP的EchoRequest數據包
         if(sendto(sockRaw , IcmpSendBuf , DEF_ICMP_PACK_SIZE , 0 , (SOCKADDR*)&destSockAddr , sizeof(destSockAddr)) == SOCKET_ERROR)
         {
            //如果目的主機不可達則直接退出
            if(WSAGetLastError() == WSAEHOSTUNREACH)
            {
                printf("目的主機不可達!\n") ;
                exit(0) ;
            }
         }
         SOCKADDR_IN from ;
         int iFromLen = sizeof(from) ;
         int iReadLen ;
         //定義接收的數據包
         char IcmpRecvBuf[MAX_ICMP_PACKET_SIZE] ;
         while(1)
         {
             iReadLen = recvfrom(sockRaw , IcmpRecvBuf , MAX_ICMP_PACKET_SIZE , 0 , (SOCKADDR*)&from , &iFromLen) ;
             if(iReadLen != SOCKET_ERROR)
             {
                 if(DecodeIcmpResponse_Ping(IcmpRecvBuf , sizeof(IcmpRecvBuf) , &stDecodeResult))
                 {
                    printf("來自 %s 的回復: 字節 = %d 時間 = %dms TTL = %d\n" , inet_ntoa(stDecodeResult.dwIPaddr) ,
                             iReadLen - 20,stDecodeResult.dwRoundTripTime ,stDecodeResult.ttl) ;
                 }
                 break ;
             }
             else if(WSAGetLastError() == WSAETIMEDOUT)
             {
                 printf("time out !  *****\n") ;
                 break ;
             }
             else
             {
                 printf("發生未知錯誤!\n") ;
                 break ;
             }
         }
         usSeqNo++ ;
       }
       //輸出屏幕信息
       printf("Ping complete...\n") ;
       closesocket(sockRaw) ;
       WSACleanup() ;
    }
    int main()
    {
       char  com[10] , IP[20] ;
       while(1){
       printf("command>>") ;
       scanf("%s %s" , com , IP) ;
       if(strcmp(com , "ping") == 0)
       {
           Ping(IP) ;
       }
       else
       {
           printf("輸入錯誤 ! \n") ;
       }
       }
       return 0 ;
    }
    

      

    參考文檔:https://zhidao.baidu.com/question/1946506262344388308.html

    https://docs.microsoft.com/zh-cn/windows/win32/api/winsock2/nf-winsock2-wsasocketa?redirectedfrom=MSDN

    https://zhidao.baidu.com/question/541753723.html

    TCP/IP網絡原理技術[清華大學出版社 周明天,汪文勇]

    互聯網控制消息協議[維基百科]

    TCP/IP詳解 卷1:協議

    TCP/IP詳解 卷2:實現

     

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

    【其他文章推薦】

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

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

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

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

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

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

  • 一文讀懂:GBDT梯度提升

    一文讀懂:GBDT梯度提升

    先縷一縷幾個關係:

    • GBDT是gradient-boost decision tree
    • GBDT的核心就是gradient boost,我們搞清楚什麼是gradient boost就可以了
    • GBDT是boost中的一種方法,boost還有XGBoost,adaboost。

    基本概念

    【Boost】就是讓多個弱分類器,通過不同的集成方式,來讓多個弱分類器變成一個強分類器。

    【gradient-boost】 梯度提升。簡單的說,先訓練一個弱分類器,然後弱分類器和目標值之間的殘差,作為下一個弱分類器訓練的目標值。這裡有一個非常簡單的例子

    • 第一個模型預測年齡,雖然真實值是30歲,第一個模型只給出了20歲的估計值;
    • 第二棵樹要預測的就是這個10歲的殘差,但是第二棵樹只給出了6歲的估計值;
    • 第三棵樹預測的是第二棵樹的4歲的殘差,但是………………(禁止套娃)

    梯度 or 殘差 ?

    對於GBDT,網上的很多文章都沒有講清楚,學習梯度還是學習殘差?從上面的那個例子來看,是學習殘差的。

    其實,從來GBDT都是學習梯度的,學習殘差只是學習梯度的一個特例!

    如果我們是在做一個回歸任務(就像是上面例子中預測年齡),採用平方損失:\(loss = \frac{1}{2}\sum^n_i{(y_i-\hat{y_i})^2}\)
    其中\(y_i\)是真實數值,\(\hat{y_i}\)是模型預測的值。

    然後想求取這個關於\(\hat{y_i}\)的梯度,那就是:
    \(\frac{\partial loss}{\partial \hat{y^i}}=(-1)(y_i-\hat{y_i})\)

    所以殘差在平方損失的情況下,就是等於負梯度,所以兩者一回事。

    殘差過於敏感

    對於數據不幹凈,沒有清晰掉異常值的數據樣本。使用平方損失對異常值過於敏感了

    所以,這裡在回歸問題中,也可以考慮使用下面的兩個損失函數:

    • Absolute loss:
      \(loss=|y-\hat{y}|\)

    • Huber loss:
      這個是設置一個閾值,當\(|y-\hat{y}|\)小於這個閾值的時候,採用平方損失,當\(|y-\hat{y}|\)大於這個閾值的時候,採用類似於絕對損失的線性損失:

      這裏看一下huber loss的函數圖像:

      就是一個平方損失,一個線性損失。

    然後看一下平方損失,絕對損失,huber損失對於異常值的容忍程度:

    CART回歸樹分裂思路(可不看)

    其實這個問題看起來問的不明所以,其實是問你決策樹如何選擇特徵的。從上面的例子可以看出來,GDBT應該是處理回歸問題的(處理連續數據的)。當然,GDBT也有辦法處理分類問題,只是這裏就不說了,這裏主要說GDBT怎麼處理回歸問題的,回歸問題能處理,那麼總有回歸離散化的辦法的

    這個問題是在問:CART TREE如何選擇特徵的CART TREE就是回歸決策樹,就是之前提到的弱分類器。

    一個決策樹,希望讀者已經有一個大概的理解了。簡單說就是:樣本可以根據的特徵A是否超過某一個閾值劃分成兩部分,然後劃分之後的每一個部分又可以根據某一個特徵是否超過某一個閾值再分成兩部分……

    這樣我們就要做出選擇了:每一個部分是根據哪一個特徵去劃分?根據這個特徵的哪一個數值作為閾值劃分?

    如果我們算力無窮,那麼自然可以遍歷每一個特徵,然後窮舉每一種可能的分割點,然後對比找到最優分割點。

    那麼如何判斷分割的點的好壞呢?得給出一個cost函數,或者叫做loss函數這樣的東西吧。

    \(loss= \sum_{第一部分}{(y_i-me an(y_{第一部分}))^2}+\sum_{第二部分}{(y_i-mean(y_{第二部分}))^2}\)

    看一下這個公式,我把公式寫的太丑了。其實這個公式非常的好理解:現在根據某一個特徵值,根據某一個閾值把樣本分成了兩個部分:第一部分和第二部分。然後計算每一個部分的樣本的label的均值,也就是公式中的:\(mean(y_{第一部分})\),\(mean(y_{第二部分})\),然後計算第一部分中所有樣本的label與第一部分label均值之間的差的平方和,同樣的過程計算第二個部分的,兩個相加起來就是這個loss。選擇能夠讓這個loss最小的分割特徵和分割閾值,就是我們要找的東西。

    其實我在學這一塊的時候,發現這個過程像是什麼?像不像聚類算法,通過上面的loss的最小化的過程,把一堆樣本分成兩類,讓兩類的類內距離最小。那個均值就像是求類中心點,計算每一個label距離類中心點的距離。(這一段看不懂也沒事

    喜歡的話請關注我們的微信公眾號~【你好世界煉丹師】。

    • 公眾號主要講統計學,數據科學,機器學習,深度學習,以及一些參加Kaggle競賽的經驗。
    • 公眾號內容建議作為課後的一些相關知識的補充,飯後甜點。
    • 此外,為了不過多打擾,公眾號每周推送一次,每次4~6篇精選文章。

    微信搜索公眾號:你好世界煉丹師。期待您的關注。

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

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

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

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

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

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

    ※回頭車貨運收費標準

  • 京都龜岡市將禁止提供塑膠袋 業者反彈起步困難

    摘錄自2020年3月17日Yahoo!新聞報導

    日本京都的龜岡市領先全國通過「塑膠袋禁止條例」,今後在當地買東西結帳,就算願意付錢也不會提供塑膠袋,擅自提供的店家還會被罰款。這項規定原本預定8月上路,但因為市民團體反應還需要適應的時間,因此目前還沒有訂下確切的實行時間。

    要改變基本生活習慣並不容易。就怕居民反對,龜岡市從去年到今年前後辦了50場說明會。但有民眾認為,要是沒和鄰近的市鎮合作,很難辦到完全禁用。根據說明會後的統計,贊成這項規定的市民高達7成5。事實上從塑膠袋要收費開始,就能看到明顯改變,帶購物袋來採買的民眾從原本的20%,大幅增加為80%。

    這一頭成功安撫民眾,另一頭還得得到超市等業者的支持。有超市業者表示,就算現在訂購紙袋,到進貨為止也要2個月。日本加盟協會負責人也表示:「像加熱的關東煮、焗烤、杯麵,得加熱湯讓客人帶走就無法賣了,毫無疑問會對業績造成影響,這可是生死存亡的問題,說真心話希望能多點緩衝時間。」

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • 「半雞半鴨」:考古發現最古老禽類化石

    摘錄自2020年3月28日大紀元報導

    考古學家發現一隻禽類生物的頭骨和腿骨化石,經檢驗發現它生活在距今約6,680萬~6,670萬年前,是至今全球發現的最古老的鳥類化石。

    這份近期發表在《自然》(Nature)期刊上的研究公布了這一發現。研究者之一劍橋大學的古生物學家菲爾德(Daniel Field)說:「這是我們至今發現的最早存在的鳥類的證據。」

    恐龍大約在距今6,600萬年前滅絕,因此這隻生物生活在僅比那個時間點早一點的時期。在此之前,科學家找到的最早的鳥類化石大約生活在距今6,650萬年前。新發現的化石比這個更早一些。

    研究人員估計這隻鳥的體重為400克左右,只有現在的水鳥——鳧的一半大。「我們認為它的臉看起來有點像現代的雞,但是頭骨的後面看起來又更像現在的鴨子。」菲爾德說。一起發現的還有它的腿骨化石,看起來它有著兩條細長的腿,說明它是生活在岸邊的一種禽類。

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

    ※超省錢租車方案

    FB行銷專家,教你從零開始的技巧

  • 幫居民生計找出路 坦尚尼亞社區林業 改善盜伐有成

    環境資訊中心綜合外電;姜唯 編譯;林大利 審校

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

    ※超省錢租車方案

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

    網頁設計最專業,超強功能平台可客製化