為什麼用抓包工具看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++ 自增語法,不提供 ++ 操作符?

在 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進階系列、好書推薦系列、技術寫作、優質英文推薦與翻譯等等,歡迎關注哦。

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

【其他文章推薦】

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

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

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

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

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

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

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併發編程的藝術-方騰飛,魏鵬,程曉明著

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

【Mongodb】 可複製集搭建

可複製集 replica set

概念圖

可複製集需要至少3個以上的mongodb節點,其中有一個主節點promary,其餘的為副本節點secondary

可複製集有三個角色:

  • 主要成員(Primary):主要接收所有寫操作。就是主節點。
  • 副本成員(Secondary):從主節點通過複製操作以維護相同的數據集,即備份數據,不可寫操作,但可以讀操作(但需要配置)。是默認的一種從節點類型。
  • 仲裁者(Arbiter):不保留任何數據的副本,只具有投票選舉作用。當然也可以將仲裁服務器維護為副本集的一部分,即副本成員同時也可以是仲裁者。也是一種從節點類型。

關於仲裁者:
如果主節點+副本節點是偶數推薦添加仲裁者,如果主節點+ 副本節點是奇數可以不添加仲裁者。仲裁者將永遠是仲裁者,而主要人員可能會退出並成為次要人員,而次要人員可能成為選舉期間的主要人員。

為什麼要用可複製集?它有什麼重要性?

  1. 避免數據丟失,保障數據安全,提高系統安全性;
    (最少3節點,最大50節點)
  2. 自動化災備機制,主節點宕機后通過選舉產生新主機;提高系統健壯性;
    (7個選舉節點上限)
  3. 讀寫分離,負載均衡,提高系統性能;

搭建

準備三個mongodb節點

正準備三個mongodb節點,我們先搭建一個主節點,2個副本節點的模式
修改配置mongo.conf

  • 第一個
systemLog:
  #MongoDB發送所有日誌輸出的目標指定為文件 
  destination: file
  #mongod或mongos應向其發送所有診斷日誌記錄信息的日誌文件的路徑 
  path: "/home/amber/mongodb/mongodb-001/log/mongod.log" 
  #當mongos或mongod實例重新啟動時,mongos或mongod會將新條目附加到現有日誌文件的末尾。 
  logAppend: true
storage: 
  #mongod實例存儲其數據的目錄。storage.dbPath設置僅適用於mongod。 
  dbPath: "/home/amber/mongodb/mongodb-001/data/db" 
  journal:
    #啟用或禁用持久性日誌以確保數據文件保持有效和可恢復。 
    enabled: true
processManagement:
  #啟用在後台運行mongos或mongod進程的守護進程模式。 
  fork: true 
  #指定用於保存mongos或mongod進程的進程ID的文件位置,其中mongos或mongod將寫入其PID 
  pidFilePath: "/home/amber/mongodb/mongodb-001/log/mongod.pid" 
net:
  #服務實例綁定所有IP,有副作用,副本集初始化的時候,節點名字會自動設置為本地域名,而不是ip 
  #bindIpAll: true 
  #服務實例綁定的IP 
  bindIp: 0.0.0.0
  #bindIp 
  #綁定的端口 
  port: 27017
replication: 
  #副本集的名稱 
  replSetName: myrs
  • 第二個第三個配置
    把上述文件中的mongodb-001換成mongodb-002``mongodb-003
    端口分別換成27018 27019

然後分別在mongodb-00X的根目錄下執行啟動命令

./bin/mongod -f ./conf/mongod.conf

檢查進程

ps -ef|grep mongod

設置主節點

進入27017的那個mongod的客戶端,並且執行

rs.initiate({
      _id: "myrs", //  需要和replSetName的名稱一致
      version: 1,
      members: [{ _id: 0, host : "192.168.xx.xx:27017" }]});

或者

rs.initiate({}) 

執行結果

提示:
1)“ok”的值為1,說明創建成功。
2)命令行提示符發生變化,變成了一個從節點角色,此時默認不能讀寫。稍等片刻,回車,變成主節  點。

配置副本節點

再27017的mongod客戶端,也就是主節點上執行192.168.xx.xx:27018是副本節點的ip和端口

rs.add("192.168.xx.xx:27018")
rs.add("192.168.xx.xx:27019")

使用

rs.status()

就可以看到members會有三個節點了

測試

再主節點插入一條數據

use article;
db.comment.insert({name: "amber"})

再從節點查看,結果

這是因為需要再從節點再次進行

rs.slaveok() // 確認當前節點是副本節點
db.comment.find(); // 查看當前數據

可以看到已經有數據了,這樣一主二從的可複製成功了

如果關閉主節點,在從節點執行rs.status();
可以看到原來的主節點的health變成了0

27018變成了新的主節點

主節點的選舉原則

MongoDB在副本集中,主節點選舉的觸發條件:
1) 主節點故障
2) 主節點網絡不可達(默認心跳信息為10秒)
3) 人工干預(rs.stepDown(600))

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

【Spring註解驅動開發】使用InitializingBean和DisposableBean來管理bean的生命周期,你真的了解嗎?

寫在前面

在《【Spring註解驅動開發】如何使用@Bean註解指定初始化和銷毀的方法?看這一篇就夠了!!》一文中,我們講述了如何使用@Bean註解來指定bean初始化和銷毀的方法。具體的用法就是在@Bean註解中使用init-method屬性和destroy-method屬性來指定初始化方法和銷毀方法。除此之外,Spring中是否還提供了其他的方式來對bean實例進行初始化和銷毀呢?

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

InitializingBean接口

1.InitializingBean接口概述

Spring中提供了一個InitializingBean接口,InitializingBean接口為bean提供了屬性初始化后的處理方法,它只包括afterPropertiesSet方法,凡是繼承該接口的類,在bean的屬性初始化后都會執行該方法。InitializingBean接口的源碼如下所示。

package org.springframework.beans.factory;
public interface InitializingBean {
	void afterPropertiesSet() throws Exception;
}

根據InitializingBean接口中提供的afterPropertiesSet()方法的名字可以推斷出:afterPropertiesSet()方法是在屬性賦好值之後調用的。那到底是不是這樣呢?我們來分析下afterPropertiesSet()方法的調用時機。

2.何時調用InitializingBean接口?

我們定位到Spring中的org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory類下的invokeInitMethods()方法中,來查看Spring加載bean的方法。

題外話:不要問我為什麼會是這個invokeInitMethods()方法,如果你和我一樣對Spring的源碼非常熟悉的話,你也會知道是這個invokeInitMethods()方法,哈哈哈哈!所以,小夥伴們不要只顧着使用Spring,還是要多看看Spring的源碼啊!Spring框架中使用了大量優秀的設計模型,其代碼的編寫規範和嚴謹程度也是業界開源框架中數一數二的,非常值得閱讀。

我們來到AbstractAutowireCapableBeanFactory類下的invokeInitMethods()方法,如下所示。

protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd)
    throws Throwable {
	//判斷該bean是否實現了實現了InitializingBean接口,如果實現了InitializingBean接口,則調用bean的afterPropertiesSet方法
    boolean isInitializingBean = (bean instanceof InitializingBean);
    if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
        if (logger.isTraceEnabled()) {
            logger.trace("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");
        }
        if (System.getSecurityManager() != null) {
            try {
                AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
                    //調用afterPropertiesSet()方法
                    ((InitializingBean) bean).afterPropertiesSet();
                    return null;
                }, getAccessControlContext());
            }
            catch (PrivilegedActionException pae) {
                throw pae.getException();
            }
        }
        else {
            //調用afterPropertiesSet()方法
            ((InitializingBean) bean).afterPropertiesSet();
        }
    }

    if (mbd != null && bean.getClass() != NullBean.class) {
        String initMethodName = mbd.getInitMethodName();
        if (StringUtils.hasLength(initMethodName) &&
            !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
            !mbd.isExternallyManagedInitMethod(initMethodName)) {
            //通過反射的方式調用init-method
            invokeCustomInitMethod(beanName, bean, mbd);
        }
    }
}

分析上述代碼后,我們可以初步得出如下信息:

  • Spring為bean提供了兩種初始化bean的方式,實現InitializingBean接口,實現afterPropertiesSet方法,或者在配置文件和@Bean註解中通過init-method指定,兩種方式可以同時使用。
  • 實現InitializingBean接口是直接調用afterPropertiesSet()方法,比通過反射調用init-method指定的方法效率相對來說要高點。但是init-method方式消除了對Spring的依賴。
  • 如果調用afterPropertiesSet方法時出錯,則不調用init-method指定的方法。

也就是說Spring為bean提供了兩種初始化的方式,第一種實現InitializingBean接口,實現afterPropertiesSet方法,第二種配置文件或@Bean註解中通過init-method指定,兩種方式可以同時使用,同時使用先調用afterPropertiesSet方法,后執行init-method指定的方法。

DisposableBean接口

1.DisposableBean接口概述

實現org.springframework.beans.factory.DisposableBean接口的bean在銷毀前,Spring將會調用DisposableBean接口的destroy()方法。我們先來看下DisposableBean接口的源碼,如下所示。

package org.springframework.beans.factory;
public interface DisposableBean {
	void destroy() throws Exception;
}

可以看到,在DisposableBean接口中只定義了一個destroy()方法。

在Bean生命周期結束前調用destory()方法做一些收尾工作,亦可以使用destory-method。前者與Spring耦合高,使用類型強轉.方法名(),效率高。後者耦合低,使用反射,效率相對低

2.DisposableBean接口注意事項

多例bean的生命周期不歸Spring容器來管理,這裏的DisposableBean中的方法是由Spring容器來調用的,所以如果一個多例實現了DisposableBean是沒有啥意義的,因為相應的方法根本不會被調用,當然在XML配置文件中指定了destroy方法,也是沒有意義的。所以,在多實例bean情況下,Spring不會自動調用bean的銷毀方法。

單實例bean案例

創建一個Animal的類實現InitializingBean和DisposableBean接口,代碼如下:

package io.mykit.spring.plugins.register.bean;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
/**
 * @author binghe
 * @version 1.0.0
 * @description 測試InitializingBean接口和DisposableBean接口
 */
public class Animal implements InitializingBean, DisposableBean {
    public Animal(){
        System.out.println("執行了Animal類的無參數構造方法");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("執行了Animal類的初始化方法。。。。。");

    }
    @Override
    public void destroy() throws Exception {
        System.out.println("執行了Animal類的銷毀方法。。。。。");

    }
}

接下來,我們新建一個AnimalConfig類,並將Animal通過@Bean註解的方式註冊到Spring容器中,如下所示。

package io.mykit.spring.plugins.register.config;

import io.mykit.spring.plugins.register.bean.Animal;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
 * @author binghe
 * @version 1.0.0
 * @description AnimalConfig
 */
@Configuration
@ComponentScan("io.mykit.spring.plugins.register.bean")
public class AnimalConfig {
    @Bean
    public Animal animal(){
        return new Animal();
    }
}

接下來,我們在BeanLifeCircleTest類中新增testBeanLifeCircle02()方法來進行測試,如下所示。

@Test
public void testBeanLifeCircle02(){
    //創建IOC容器
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AnimalConfig.class);
    System.out.println("IOC容器創建完成...");
    //關閉IOC容器
    context.close();
}

運行BeanLifeCircleTest類中的testBeanLifeCircle02()方法,輸出的結果信息如下所示。

執行了Animal類的無參數構造方法
執行了Animal類的初始化方法。。。。。
IOC容器創建完成...
執行了Animal類的銷毀方法。。。。。

從輸出的結果信息可以看出:單實例bean下,IOC容器創建完成后,會自動調用bean的初始化方法;而在容器銷毀前,會自動調用bean的銷毀方法。

多實例bean案例

多實例bean的案例代碼基本與單實例bean的案例代碼相同,只不過在AnimalConfig類中,我們在animal()方法上添加了@Scope(“prototype”)註解,如下所示。

package io.mykit.spring.plugins.register.config;
import io.mykit.spring.plugins.register.bean.Animal;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
/**
 * @author binghe
 * @version 1.0.0
 * @description AnimalConfig
 */
@Configuration
@ComponentScan("io.mykit.spring.plugins.register.bean")
public class AnimalConfig {
    @Bean
    @Scope("prototype")
    public Animal animal(){
        return new Animal();
    }
}

接下來,我們在BeanLifeCircleTest類中新增testBeanLifeCircle03()方法來進行測試,如下所示。

@Test
public void testBeanLifeCircle03(){
    //創建IOC容器
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AnimalConfig.class);
    System.out.println("IOC容器創建完成...");
    System.out.println("-------");
    //調用時創建對象
    Object bean = ctx.getBean("animal");
    System.out.println("-------");
    //調用時創建對象
    Object bean1 = ctx.getBean("animal");
    System.out.println("-------");
    //關閉IOC容器
    ctx.close();
}

運行BeanLifeCircleTest類中的testBeanLifeCircle03()方法,輸出的結果信息如下所示。

IOC容器創建完成...
-------
執行了Animal類的無參數構造方法
執行了Animal類的初始化方法。。。。。
-------
執行了Animal類的無參數構造方法
執行了Animal類的初始化方法。。。。。
-------

從輸出的結果信息中可以看出:在多實例bean情況下,Spring不會自動調用bean的銷毀方法。

好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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是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/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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