標籤: USB CONNECTOR

  • 阿裏面試官最喜歡問的21個HashMap面試題

    阿裏面試官最喜歡問的21個HashMap面試題

    1.HashMap 的數據結構?

    A:哈希表結構(鏈表散列:數組+鏈表)實現,結合數組和鏈表的優點。當鏈表長度超過 8 時,鏈錶轉換為紅黑樹。

    transient Node<K,V>\[\] table;
    

    2.HashMap 的工作原理?

    HashMap 底層是 hash 數組和單向鏈表實現,數組中的每個元素都是鏈表,由 Node 內部類(實現 Map.Entry接口)實現,HashMap 通過 put & get 方法存儲和獲取。

    存儲對象時,將 K/V 鍵值傳給 put() 方法:

    ①、調用 hash(K) 方法計算 K 的 hash 值,然後結合數組長度,計算得數組下標;

    ②、調整數組大小(當容器中的元素個數大於 capacity * loadfactor 時,容器會進行擴容resize 為 2n);

    ③、i.如果 K 的 hash 值在 HashMap 中不存在,則執行插入,若存在,則發生碰撞;

    ii.如果 K 的 hash 值在 HashMap 中存在,且它們兩者 equals 返回 true,則更新鍵值對;

    iii. 如果 K 的 hash 值在 HashMap 中存在,且它們兩者 equals 返回 false,則插入鏈表的尾部(尾插法)或者紅黑樹中(樹的添加方式)。(JDK 1.7 之前使用頭插法、JDK 1.8 使用尾插法)(注意:當碰撞導致鏈表大於 TREEIFY_THRESHOLD = 8 時,就把鏈錶轉換成紅黑樹)

    獲取對象時,將 K 傳給 get() 方法:①、調用 hash(K) 方法(計算 K 的 hash 值)從而獲取該鍵值所在鏈表的數組下標;②、順序遍歷鏈表,equals()方法查找相同 Node 鏈表中 K 值對應的 V 值。

    hashCode 是定位的,存儲位置;equals是定性的,比較兩者是否相等。

    3.當兩個對象的 hashCode 相同會發生什麼?

    因為 hashCode 相同,不一定就是相等的(equals方法比較),所以兩個對象所在數組的下標相同,”碰撞”就此發生。又因為 HashMap 使用鏈表存儲對象,這個 Node 會存儲到鏈表中。

    4.你知道 hash 的實現嗎?為什麼要這樣實現?

    JDK 1.8 中,是通過 hashCode() 的高 16 位異或低 16 位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度,功效和質量來考慮的,減少系統的開銷,也不會造成因為高位沒有參与下標的計算,從而引起的碰撞。

    5.為什麼要用異或運算符?

    保證了對象的 hashCode 的 32 位值只要有一位發生改變,整個 hash() 返回值就會改變。盡可能的減少碰撞。

    6.HashMap 的 table 的容量如何確定?loadFactor 是什麼?該容量如何變化?這種變化會帶來什麼問題?

    ①、table 數組大小是由 capacity 這個參數確定的,默認是16,也可以構造時傳入,最大限制是1<<30;

    ②、loadFactor 是裝載因子,主要目的是用來確認table 數組是否需要動態擴展,默認值是0.75,比如table 數組大小為 16,裝載因子為 0.75 時,threshold 就是12,當 table 的實際大小超過 12 時,table就需要動態擴容;

    ③、擴容時,調用 resize() 方法,將 table 長度變為原來的兩倍(注意是 table 長度,而不是 threshold)

    ④、如果數據很大的情況下,擴展時將會帶來性能的損失,在性能要求很高的地方,這種損失很可能很致命。

    7.HashMap中put方法的過程?

    答:“調用哈希函數獲取Key對應的hash值,再計算其數組下標;
    如果沒有出現哈希衝突,則直接放入數組;如果出現哈希衝突,則以鏈表的方式放在鏈表後面;
    如果鏈表長度超過閥值( TREEIFY THRESHOLD==8),就把鏈錶轉成紅黑樹,鏈表長度低於6,就把紅黑樹轉回鏈表;
    如果結點的key已經存在,則替換其value即可;
    如果集合中的鍵值對大於12,調用resize方法進行數組擴容。”

    8.數組擴容的過程?

    創建一個新的數組,其容量為舊數組的兩倍,並重新計算舊數組中結點的存儲位置。結點在新數組中的位置只有兩種,原下標位置或原下標+舊數組的大小。

    9.拉鏈法導致的鏈表過深問題為什麼不用二叉查找樹代替,而選擇紅黑樹?為什麼不一直使用紅黑樹?

    之所以選擇紅黑樹是為了解決二叉查找樹的缺陷,二叉查找樹在特殊情況下會變成一條線性結構(這就跟原來使用鏈表結構一樣了,造成很深的問題),遍歷查找會非常慢。

    而紅黑樹在插入新數據后可能需要通過左旋,右旋、變色這些操作來保持平衡,引入紅黑樹就是為了查找數據快,解決鏈表查詢深度的問題,我們知道紅黑樹屬於平衡二叉樹,但是為了保持“平衡”是需要付出代價的,但是該代價所損耗的資源要比遍歷線性鏈表要少,所以當長度大於8的時候,會使用紅黑樹,如果鏈表長度很短的話,根本不需要引入紅黑樹,引入反而會慢。

    10.說說你對紅黑樹的見解?

    • 每個節點非紅即黑
    • 根節點總是黑色的
    • 如果節點是紅色的,則它的子節點必須是黑色的(反之不一定)
    • 每個恭弘=叶 恭弘子節點都是黑色的空節點(NIL節點)
    • 從根節點到恭弘=叶 恭弘節點或空子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)

    11.jdk8中對HashMap做了哪些改變?

    在java 1.8中,如果鏈表的長度超過了8,那麼鏈表將轉換為紅黑樹。(桶的數量必須大於64,小於64的時候只會擴容)

    發生hash碰撞時,java 1.7 會在鏈表的頭部插入,而java 1.8會在鏈表的尾部插入

    在java 1.8中,Entry被Node替代(換了一個馬甲)。

    12.HashMap,LinkedHashMap,TreeMap 有什麼區別?

    HashMap 參考其他問題;

    LinkedHashMap 保存了記錄的插入順序,在用 Iterator 遍歷時,先取到的記錄肯定是先插入的;遍歷比 HashMap 慢;

    TreeMap 實現 SortMap 接口,能夠把它保存的記錄根據鍵排序(默認按鍵值升序排序,也可以指定排序的比較器)

    13.HashMap & TreeMap & LinkedHashMap 使用場景?

    一般情況下,使用最多的是 HashMap。

    HashMap:在 Map 中插入、刪除和定位元素時;

    TreeMap:在需要按自然順序或自定義順序遍歷鍵的情況下;

    LinkedHashMap:在需要輸出的順序和輸入的順序相同的情況下。

    14.HashMap 和 HashTable 有什麼區別?

    ①、HashMap 是線程不安全的,HashTable 是線程安全的;

    ②、由於線程安全,所以 HashTable 的效率比不上 HashMap;

    ③、HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null,而 HashTable不允許;

    ④、HashMap 默認初始化數組的大小為16,HashTable 為 11,前者擴容時,擴大兩倍,後者擴大兩倍+1;

    ⑤、HashMap 需要重新計算 hash 值,而 HashTable 直接使用對象的 hashCode

    15.Java 中的另一個線程安全的與 HashMap 極其類似的類是什麼?同樣是線程安全,它與 HashTable 在線程同步上有什麼不同?

    ConcurrentHashMap 類(是 Java併發包 java.util.concurrent 中提供的一個線程安全且高效的 HashMap 實現)。

    HashTable 是使用 synchronize 關鍵字加鎖的原理(就是對對象加鎖);

    而針對 ConcurrentHashMap,在 JDK 1.7 中採用 分段鎖的方式;JDK 1.8 中直接採用了CAS(無鎖算法)+ synchronized。

    16.HashMap & ConcurrentHashMap 的區別?

    除了加鎖,原理上無太大區別。另外,HashMap 的鍵值對允許有null,但是ConCurrentHashMap 都不允許。

    17.為什麼 ConcurrentHashMap 比 HashTable 效率要高?

    HashTable 使用一把鎖(鎖住整個鏈表結構)處理併發問題,多個線程競爭一把鎖,容易阻塞;

    ConcurrentHashMap

    • JDK 1.7 中使用分段鎖(ReentrantLock + Segment + HashEntry),相當於把一個 HashMap 分成多個段,每段分配一把鎖,這樣支持多線程訪問。鎖粒度:基於 Segment,包含多個 HashEntry。
    • JDK 1.8 中使用 CAS + synchronized + Node + 紅黑樹。鎖粒度:Node(首結點)(實現 Map.Entry)。鎖粒度降低了。

    18.針對 ConcurrentHashMap 鎖機制具體分析(JDK 1.7 VS JDK 1.8)?

    JDK 1.7 中,採用分段鎖的機制,實現併發的更新操作,底層採用數組+鏈表的存儲結構,包括兩個核心靜態內部類 Segment 和 HashEntry。

    ①、Segment 繼承 ReentrantLock(重入鎖) 用來充當鎖的角色,每個 Segment 對象守護每個散列映射表的若干個桶;

    ②、HashEntry 用來封裝映射表的鍵-值對;

    ③、每個桶是由若干個 HashEntry 對象鏈接起來的鏈表

    JDK 1.8 中,採用Node + CAS + Synchronized來保證併發安全。取消類 Segment,直接用 table 數組存儲鍵值對;當 HashEntry 對象組成的鏈表長度超過 TREEIFY_THRESHOLD 時,鏈錶轉換為紅黑樹,提升性能。底層變更為數組 + 鏈表 + 紅黑樹。

    19.ConcurrentHashMap 在 JDK 1.8 中,為什麼要使用內置鎖 synchronized 來代替重入鎖 ReentrantLock?

    ①、粒度降低了;

    ②、JVM 開發團隊沒有放棄 synchronized,而且基於 JVM 的 synchronized 優化空間更大,更加自然。

    ③、在大量的數據操作下,對於 JVM 的內存壓力,基於 API 的 ReentrantLock 會開銷更多的內存。

    20.ConcurrentHashMap 簡單介紹?

    ①、重要的常量:

    private transient volatile int sizeCtl;

    當為負數時,-1 表示正在初始化,-N 表示 N – 1 個線程正在進行擴容;

    當為 0 時,表示 table 還沒有初始化;

    當為其他正數時,表示初始化或者下一次進行擴容的大小。

    ②、數據結構:

    Node 是存儲結構的基本單元,繼承 HashMap 中的 Entry,用於存儲數據;

    TreeNode 繼承 Node,但是數據結構換成了二叉樹結構,是紅黑樹的存儲結構,用於紅黑樹中存儲數據;

    TreeBin 是封裝 TreeNode 的容器,提供轉換紅黑樹的一些條件和鎖的控制。

    ③、存儲對象時(put() 方法):

    如果沒有初始化,就調用 initTable() 方法來進行初始化;

    如果沒有 hash 衝突就直接 CAS 無鎖插入;

    如果需要擴容,就先進行擴容;

    如果存在 hash 衝突,就加鎖來保證線程安全,兩種情況:一種是鏈表形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結構插入;

    如果該鏈表的數量大於閥值 8,就要先轉換成紅黑樹的結構,break 再一次進入循環

    如果添加成功就調用 addCount() 方法統計 size,並且檢查是否需要擴容。

    ④、擴容方法 transfer():默認容量為 16,擴容時,容量變為原來的兩倍。

    helpTransfer():調用多個工作線程一起幫助進行擴容,這樣的效率就會更高。

    ⑤、獲取對象時(get()方法):

    計算 hash 值,定位到該 table 索引位置,如果是首結點符合就返回;

    如果遇到擴容時,會調用標記正在擴容結點 ForwardingNode.find()方法,查找該結點,匹配就返回;

    以上都不符合的話,就往下遍歷結點,匹配就返回,否則最後就返回 null。

    21.ConcurrentHashMap 的併發度是什麼?

    程序運行時能夠同時更新 ConccurentHashMap 且不產生鎖競爭的最大線程數。默認為 16,且可以在構造函數中設置。

    當用戶設置併發度時,ConcurrentHashMap 會使用大於等於該值的最小2冪指數作為實際併發度(假如用戶設置併發度為17,實際併發度則為32)

    更多精彩面試題

    如果有想看的小夥伴就給我留言吧。這就是本文的全部內容了。如果覺得寫的不錯,請記得收藏加轉發。還想跟我看更多數據結構和算法題的小夥伴們,記得關注我公眾號:程序零世界,Java 就這麼回事。

    線程,多線程,線程池,線程上下文,鎖一鍵啟動線程

    紅黑樹其實並不難,只是你還沒看過ta

    JVM其實並沒有那麼難,你也該啃下TA了

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

    【其他文章推薦】

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

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

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

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

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

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

  • 驗證碼原理及驗證

    驗證碼的原理

    驗證碼的作用:

     驗證碼是是一種區分用戶是計算機還是人的公共全自動程序,可以防止:惡意破解密碼、刷票、論壇灌水、有效防止某個黑客對某一特定註冊用戶,用特定程序暴力破解方式進行不斷的登錄嘗試。實際上驗證碼是現在很多網站通行的方式,我們利用比較簡易的方式實現了這個功能。

    生成驗證碼

    生成驗證碼這個功能已經特別成熟了 在網上可以找到很多資源

    以下是生成驗證碼的相關代碼:

    package com._yhnit.randomcode;
    
    import java.awt.Color;
    import java.awt.Font;
    import java.awt.Graphics;
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    import java.util.Random;
    
    import javax.imageio.ImageIO;
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    /**
     *  生成驗證碼的Servlet
     * @author yhn
     *
     */
    @WebServlet("/createRandomcode")
    public class RandomCodeServlet extends HttpServlet{
    
    	private static final long serialVersionUID = 1L;
    	 public RandomCodeServlet() {
    	        super();
    
    	    }
    
        protected void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
    
            doPost(request, response);
        }
    
        protected void doPost(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
    
            // 響應頭信息
            response.setHeader("Pragma", "No-Cache");
            response.setHeader("Cache-Control", "no-cache");
            response.setDateHeader("Expries", 0);
    
            // 隨機數生成類
            Random random = new Random();
    
            // 定義驗證碼的位數
            int size = 5;
    
            // 定義變量保存生成的驗證碼
            String vCode = "";
            char c;
            // 產生驗證碼
            for (int i = 0; i < size; i++) {
                // 產生一個26以內的隨機整數
                int number = random.nextInt(26);
                // 如果生成的是偶數,則隨機生成一個数字
                if (number % 2 == 0) {
                    c = (char) ('0' + (char) ((int) (Math.random() * 10)));
                    // 如果生成的是奇數,則隨機生成一個字母
                } else {
                    c = (char) ((char) ((int) (Math.random() * 26)) + 'A');
                }
                vCode = vCode + c;
            }
    
            // 保存生成的5位驗證碼
            request.getSession().setAttribute("RANDOMCODE_IN_SESSION", vCode);
    
            // 驗證碼圖片的生成
            // 定義圖片的寬度和高度
            int width = (int) Math.ceil(size * 20);
            int height = 30;
            BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            // 獲取圖片的上下文
            Graphics gr = image.getGraphics();
            // 設定圖片背景顏色
            gr.setColor(Color.WHITE);
            gr.fillRect(0, 0, width, height);
            // 設定圖片邊框
            gr.setColor(Color.GRAY);
            gr.drawRect(0, 0, width - 1, height - 1);
            // 畫十條幹擾線
            for (int i = 0; i < 5; i++) {
                int x1 = random.nextInt(width);
                int y1 = random.nextInt(height);
                int x2 = random.nextInt(width);
                int y2 = random.nextInt(height);
                gr.setColor(randomColor());
                gr.drawLine(x1, y1, x2, y2);
            }
            // 設置字體,畫驗證碼
            gr.setColor(randomColor());
            gr.setFont(randomFont());
            gr.drawString(vCode, 10, 22);
            // 圖像生效
            gr.dispose();
            // 輸出到頁面
            ImageIO.write(image, "JPEG", response.getOutputStream());
    
        }
    
        // 生成隨機的顏色
        private Color randomColor() {
            int red = r.nextInt(150);
            int green = r.nextInt(150);
            int blue = r.nextInt(150);
            return new Color(red, green, blue);
        }
    
        private String[] fontNames = { "宋體", "華文楷體", "黑體", "微軟雅黑", "楷體_GB2312" };
        private Random r = new Random();
    
        // 生成隨機的字體
        private Font randomFont() {
            int index = r.nextInt(fontNames.length);
            String fontName = fontNames[index];// 生成隨機的字體名稱
            int style = r.nextInt(4);
            int size = r.nextInt(3) + 24; // 生成隨機字號, 24 ~ 28
            return new Font(fontName, style, size);
        }
    }
    
    
    
    

    上述代碼中 定義了生成5位数字+字母的驗證碼

    生成的驗證碼 將存放到兩個地方:

    1. Session中
    2. 放到圖片上去

    最重要的是 將驗證碼存入Session,因為後台校驗驗證碼是否正確要依靠這一步

    // 保存生成的5位驗證碼
     request.getSession().setAttribute("RANDOMCODE_IN_SESSION", vCode);
    

    前端頁面實現驗證碼的切換

    在很多應用中 ,我們都會看見驗證碼的切換操作

    比如:點擊圖片切換,或者點擊後面文字(類如 看不清,換一張) 進行切換

    其實 切換很簡單 只是將圖片元素 的src 屬性 變換一下就可以完成

    這裏給驗證碼圖片 和 換一張文字添加點擊事件

    驗證碼:<input type="text" maxlength="5" required="required" name ="randomcode">
    	   <img  src="/createRandomcode" style="cursor: pointer;" onclick="change();"  id="randomcodeImg">
    		<a href="" onclick="change();">換一張</a><br>
    
    

    點擊事件 是一個名字為change函數

    function change(){
    	// 因為有緩存  所以加一個隨機數  表示不同的請求
    	document.getElementById("randomcodeImg").src="/createRandomcode?"+new Date().getTime();	
    	}
    

    注意這裏:src不能也寫 /createRandomcode,因為瀏覽器有緩存 因為之前的src就是它

    所以點擊時不會發生切換,所以我們可以加個隨機數代表每一次都是一個新的請求。

    這樣就可以實現驗證碼的切換了。

    驗證碼的後台驗證

    驗證其實也很簡單,只需要把輸入的和圖片中的驗證碼進行對比即可

    獲取輸入的驗證碼:

    String code = req.getParameter("randomcode");
    

    獲取圖片中的驗證碼:

    (生成的時候 已經存在Session中 這時只需要從Session中取出即可)

    String Imgcode = req.getSession().getAttribute("RANDOMCODE_IN_SESSION").toString();
    

    兩者進行對比驗證:

    if (!code.equalsIgnoreCase(Imgcode)) {
        // 設置一些錯誤提示  提示用戶輸入錯誤 
    	req.getSession().setAttribute("errorMes", "請輸入正確的驗證碼或已經過期");
    	req.getRequestDispatcher("randomcode/RandomCodeLogin.jsp").forward(req, resp);
    	return;
    }
    		
    // 此時驗證碼成功
    System.out.println("驗證碼成功");
    // 避免重複提交  去除Session中這一次驗證碼
    req.getSession().removeAttribute("RANDOMCODE_IN_SESSION");
    
    // 繼續驗證用戶名和密碼  ....
    

    驗證碼驗證成功之後 要銷毀Session中這次的驗證碼(驗證碼一次性使用) 避免重複提交

    // 避免重複提交  去除Session中這一次驗證碼
    req.getSession().removeAttribute("RANDOMCODE_IN_SESSION");
    

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

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

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

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

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

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

    ※回頭車貨運收費標準

  • 走出舒適圈的信念和勇氣——“Learning by doing!” 我的軟工2020春季教學總結

          看着大家陸續提交個人學期總結,我還不敢去翻看,怕思緒紛飛思維定式,變了自己寫總結的初心和思路。一篇總結,開頭起筆尤是最難,總是想各式各樣的開頭,翻來覆去,寫了念,念了刪,寫了念,念了刪。還是回到初心,回首一下為什麼上這門課。經過一個學期,我的態度和想法有沒有變化,我的收穫和驚喜是什麼。

           為什麼我想上這個班?
           過去沒有一次課,會像這次一樣,具有一點使命感。過去也沒有一次課,全程都是線上進行。也沒有一次課,如果放大忐忑的心情,也會挺忐忑的。
           作為一門臨時穿插安排的選修課,上課前,我聽到的關於系裡同學的水平和积極主動性,負向居多。如果課程要求高和嚴,還可能一翻兩瞪眼,學生投訴或紛紛消極放棄,那麼我大多數的時間精力可能都會用來勸說、解釋和雞湯。如果上的太差,也有可能搞砸後續我想進行的課程教學改革和質量提升。雖然考慮過這些困難,但相比於還沒有為系裡學生完整上過一門課,還沒有近距離感受不同學生的精彩作品和風格,還沒有為後續其他課程的改革做探索和調研,前面那些疑慮早已經拋到九霄雲外,只有許多期待和興奮感。

           你們給我的驚喜:

           你們從個人Github開始,也結束於團隊協作的GitHub,而且不少組做的很不錯,互幫互學,其團隊Github實踐能力,遠勝過以往我的班級;
           你們其中有的組分享的Android或小程序開發的經驗和教程,寫的用心,媲美以往我的班級;
           你們不少組用上了一些自動化測試工具,而且妥帖,用的好,遠勝過以往我的班級;
           更難能可貴的是,你們的不少作品,都離用戶很近,教務課程表、查寢點名、圖書館佔座、校園失物招領……等等,都將可以被用得上,希望你們不斷將作品成型,離之更近。
           不少同學的能力和潛質,都讓我覺得相見恨晚,也相識太短。匆匆你們也就要進入畢業年級。什麼是課程?就是拋卻那些具體的什麼理論和知識,回憶自己能留下的,就是這門課要教給你的。 如果問我,我們短暫的線上相聚,這門課,要交給你們什麼呢?是“Learing by doing”嗎? 這也許是之一。做中學,其實就是做自己所不會的。我們常有的觀念是,我不會,所以我做不了。而“Learning by doing”給我們的勇氣和信念是:做我所不會的,但又是對於自己發展非常重要的,甚至是關鍵路徑上的實現和突破。換句簡單的話來說,這門課想交(交,不是教,我沒有寫錯別字)給你們的是:走出舒適圈。其中的信念和勇氣就是,我可以“Learning by doing!”

          你們給了我更多在專業推行課程實踐改革的信心和動力。信心是:你們有這麼多潛質和能力,怎麼就不能做成項目,達成自己的能力提升呢? 動力是:在你們最好的時節,遇到你們,如果我們不能抓住這樣的機會,把握這樣的機會,做的更好,錯過了,將可惜許多未來的你們。成為系裡不少項目的開發者、成為課程核心助教、成為我們改革的初創者和開拓者,是這門課和這個學期,你們給我的最大收穫。

          就算是自賦的使命感吧,我想,當初來,並不是讓自己來掙課時費的。希望能了解現狀,立足現實,理解問題,分析原因,給出方法,執意推行,做出成果。前四點,無論老師或是學生,多多少少都有感慨和認知。不少知名的企業家,也常常在不同場合,對義務教育、高等教育提出屬於自己的真知灼見,大多數時是痛心疾首,哀其不幸,怒其不爭,覺得應該這樣改那樣改。為什麼問題顯而易見,現狀人人不滿(至少是不滿意),但改變卻牛步而行,各層次教育依然故我。人人都能對教育發表評論,因為重要,教育關係千家萬戶,關係國計民生,關係百年基業;也因為平凡,人人都受過教育,當過學生,也大多教育別人(比如養兒育女)。但這些其實是一種錯覺。企業也很重要啊,但少有人能夠對企業經營管理指摘或評論,很少其他人指導企業經營者應該怎麼做。我想,原因可能在距離。是否直接與學生互動,感受和方法,可能會真的不一樣。教學不是做菜,學生不是食材,互動勝過一切。這也是慕課為什麼知易行難,選課人數多,堅持人數微乎其微的原因。即使堅持了,效果也遠遠不如近距離教學的收穫和感受。評論或建議教育的人的錯覺,就在於希望自己的想法能被教育者一以貫之,卻常常忽略受教育者的感受、過程、反饋和互動。教學相長,如同沒有一次軟件開發項目是可以完全一樣完全照搬的,也沒有一次課程教學是可以完全一樣完全照搬的。更沒有什麼理論或建議,是可以醍醐灌頂,直接有效快速解決教育難題的。孔聖人之所以較其他名家更偉大一些,稱為至聖先師,除了有真知灼見,更在於自己帶領弟子三千,是真正戰鬥在教育一線的教育者。與學生的互動,一問一答,一段經歷,一個故事,乃至於對學生的點評,都成為後人傳頌的至理名言。將距離拉近,師生作為教學相長的團隊一體,才能相互促進,提升和改變。
         《構建之法》的作者,也正是將在清華、北航、微軟亞洲研究院的教學實踐的課程講義,凝結成書,並不斷在教學實踐中推陳出新,改版完善,才讓書具有了強大和茁壯的生命力,書中凝練的教學做法不斷推廣鋪開和可持續化,成為不少其他教學研討場合里都會提到的話題。回到根源,是經過實踐檢驗的“Learning by doing”才具有了這樣的生命力。如何繼往開來,我想,也要回到這樣的初心。不斷實踐,不斷改變。比如,線上教學,是可以發揮這樣的優勢的。作者對某些博客夜以繼日或苦心費思的點評,礙於單篇博客本身的閱讀量,點評被看到率不算高,回復率更讓人着急,也不一定能夠符合這個時代視頻影音影響力的特點。所以,正是線上教學,視頻直播,考慮到傳播力、影響力、互動性,作者可以考慮不斷前進,為不同開展的學校,做一次線上互動的軟工講座,分享軟件工程思維、案例、心得,和互動問答,這些,都將長久影響不少學生,也能逐步累積與一線學生互動的思考、感受和來自廣大讀者受眾的聲音。對作者來說,這一步,其實也是走出舒適圈,“Learning by doing!”

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

    【其他文章推薦】

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

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

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

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

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

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

  • Web前端兼容性指南

    Web前端兼容性指南

    一、Web前端兼容性問題

    一直以來,Web前端領域最大的問題就是兼容性問題,沒有之一。

     

    前端兼容性問題分三類:

    • 瀏覽器兼容性
    • 屏幕分辨率兼容性
    • 跨平台兼容性

     

    1、瀏覽器兼容性問題

    第一次瀏覽器大戰發生在上個世紀90年代,微軟發布了IE瀏覽器,和網景公司的Netscape Navigator大打出手,1998年網景不得不將公司賣給AOL。沒有了對手的IE不思進取,W3C標準支持發展緩慢,為以後的IE兼容性災難埋下了伏筆。到2004年,IE的市場份額達到95%,但在此之後IE的份額逐步遭其他瀏覽器蠶食,主要包括Firefox,Chrome,Safari和Opera。.

     

    2001年8月27日,微軟發布IE6,時隔五年直到2006年才發布了IE7。2009年3月19日,經歷了眾多測試版后,IE8最終發布,雖然IE8針對舊版IE在多方面做了很大改進,但在HTML5、CSS 3等標準支持方面仍落後於其他瀏覽器對手。這三個版本的IE是所有兼容性問題的最大根源,堪稱前端噩夢。

     

    IE6、7、8不支持HTML5、CSS3、SVG標準,可被判定為“極難兼容”

    IE9不支持Flex、Web Socket、WebGL,可被判定為“較難兼容”

    IE10部分支持Flex(-ms-flexbox)、Web Socket,可被判定為“較易兼容”

    IE11部分支持Flex、WebGL,可被判定為“較易兼容”

     

    IE6、7、8、9可視為“老式瀏覽器”

    IE10、11可視為“准現代瀏覽器”

    Chrome、Firefox、Safari、Opera 、Edge可視為“現代瀏覽器”

     

    瀏覽器與Windows版本份額

    Statcounter的各項數據以2020年6月為基準。

    http://gsa.statcounter.com/

     

     

     2、屏幕分辨率兼容性問題

    在不同的屏幕分辨率,瀏覽器頁面展示差異很大。特別是屏幕分辨率較小時,容易發生布局錯亂。為了解決這個問題,響應式UI框架應運而生。

     

    主流桌面屏幕分辨率寬度集中在1280~1920,高度集中在720~1080;

    主流平板屏幕分辨率寬度集中在962~1280,高度集中在601~800。

    主流移動屏幕分辨率寬度集中在360~414,高度集中在640~896。

     

    典型的桌面屏幕分辨率:1920×1080

    典型的便攜屏幕分辨率:1366×768

    典型的平板屏幕分辨率:768×1024

    典型的移動屏幕分辨率:360×640

     

    Bootstrap定義(參考系是邏輯分辨率):

    分辨率

    設備名

    典型屏幕

    >=1400px

    xxl 超超大屏設備

    桌面屏幕

    >=1200px

    xl 超大屏設備

    便攜屏幕

    >=992px

    lg 大屏設備

    豎屏桌面屏幕、橫屏平板屏幕

    >=768px

    md 中屏設備

    豎屏平板屏幕

    >=576px

    sm 小屏設備

    橫屏移動屏幕

    <576px

    xs 超小屏(自動)設備

    豎屏移動屏幕

    注:Bootstrap5新增xxl,Bootstrap3中的lg>=1200px,無576px檔。

     

    手機屏幕分辨率說明

    由於手機屏幕尺寸過小,使用原始分辨率會使得頁面显示過小,因此使用了邏輯分辨率,用倍數放大的方法來保證兼容性。比如iOS app的UI資源區分@1x、@2x和@3x,這就是指原始分辨率對邏輯分辨率的倍數,被稱為設備像素比DPR。所以大部分人的手機分辨率都是1080×1920,在分類中卻被歸為了360×640。這個分辨率和CSS中的PX是一致的。

     

    桌面屏幕分辨率說明

    移動設備一開始就考慮了DPR,而Windwos桌面的分辨率由於歷史原因卻沒有這一概念,於是Windwos引入了DPI,最初是設置DPI,後來是設置DPI比例。比如設置DPI比例=125%,你可以查詢Chrome的window.devicePixelRatio,這時輸出1.25,這說明DPI比例=DPR。但是大部分老程序並不支持DPI(Unaware),所以當你設置高DPI時,只能等比放大,字模糊到眼要瞎,最後落得空有大屏只能用超低分辨率。由於Chrome支持DPI,所以並不擔心Web有DPI問題。但需要注意的是與手機屏幕分辨率不同,桌面分辨率要除以DPI比例,才是邏輯分辨率。如1920×1080設置DPI比例=1.25,邏輯分辨率實際為1536×864。

      

      

    屏幕分辨率基礎概念說明

    縮寫

    全稱

    說明

    PX

    Device Pixels

    設備像素,指設備的物理像素

    PX

    CSS Pixels

    CSS像素,指CSS樣式代碼中使用的邏輯像素

    DOT

    Dot

    點,屏幕或打印紙上的點,等同物理像素

    PT

    Point

    磅(傳統長度單位)為1/72英寸=0.35mm

    PT

    iOS Point

    磅(iOS長度單位),為1/163英寸,等同於CSS邏輯像素

    DP

    Density independent Pixels

    設備無關像素(Android長度單位),為1/160英寸,等同於CSS邏輯像素

    SP

    Scale independent Pixels

    縮放無關像素(Android字體單位),等同於CSS邏輯像素,但文字尺寸可調(單獨縮放)

    DPR

    Device Pixel Ratio

    設備像素比,指CSS邏輯像素對於物理像素的倍數

    DPPX

    Dots Per Pixel

    等同於DPR

    PPI

    Pixel Per Inch

    屏幕上每英寸(2.54厘米)的像素點個數

    DPI

    Dots Per Inch

    屏幕或紙上每英寸(2.54厘米)的點個數,標準密度:傳統打印=72;Windows=96;Android=160;iOS=163。

    DPIR

    DPI Ratio

    DPI縮放比例,指DPI對於Windows標準DPI的倍數=DPI/96,等同於DPR

    注:各廠商概念有重名現象,請注意區分。

     

    各平台屏幕分辨率份額

      

    3、跨平台兼容性問題 

    隨着移動和平板市場的日益發展,Web在桌面、平板、移動平台上的兼容性問題日益突出。由於移動和平板是觸摸式操作,與桌面的鼠標操作方式有很大差異,因此在不同平台上要做相應修改。為了解決這個問題,誕生了跨平台框架,在不同平台上,外觀、布局、操作都有差異化修改。

     

    各平台份額

      

    二、前端里程碑框架

    在前端領域,隨着技術的不斷進步,逐步誕生了一些里程碑式的前端框架。這些前端框架,大致也是隨着兼容性問題的發生、發展而誕生、發展的。

     

    這些框架代表了前端應用當時先進、成熟、主流的開發方式與發展方向,兼容性問題也在這些框架的基礎之上不斷得到解決,大致也分為三個階段:

    一、DOM操作框架,代表框架:jQuery

    二、響應式框架,代表框架:Bootstrap

    三、前端MVC框架,代表框架:React、Angular、Vue

     

    1、JQuery

    2006年1月John Resig等人創建了jQuery;8月,jQuery的第一個穩定版本。jQuery是DOM操作時代前端框架最優秀,也幾乎是唯一代表;但是在以React為代表的新式前端框架崛起之後,迅速沒落。

     

    • JQuery 1.x兼容IE6+瀏覽器
    • JQuery 2.x兼容IE9+瀏覽器
    • JQuery 3.x兼容IE9+瀏覽器

     

    2、Bootstrap

    Bootstrap原名Twitter Blueprint,由Mark Otto和Jacob Thornton開發,最經典的響應式CSS框架,在2011年8月19日作為開源項目發布。其核心是16列布局柵格系統,使用媒體查詢設定閾值為超小屏幕,小屏幕,中等屏幕,大屏幕,超大屏幕創建不同的樣式。

     

    • Bootstrap 2兼容IE7+瀏覽器
    • Bootstrap 3兼容IE8+瀏覽器
    • Bootstrap 4兼容IE10+瀏覽器
    • Bootstrap 5不兼容IE瀏覽器

     

    3、React

    React 起源於 Facebook 的內部項目,在前端MVC框架大潮中誕生並走紅。2013年5月開源,憑藉Virtual Dom,JSX,Flux,Native等一大批創新特性,迅速吸引了大量開發人員,至今仍是最先進的前端JS框架。

     

    4、Angular

    AngularJS 誕生於2009年,由Misko Hevery 等人創建,後為Google所收購。由於Google不差錢,所以AngularJS經歷顛覆性升級為Angular。Angular最大的特點就是大而全。

     

    5、Vue

    2013年,在Google工作的尤雨溪,受到Angular的啟發,從中提取自己所喜歡的部分,開發出了一款輕量框架,最初命名為Seed,后更名為Vue。

     

    三、瀏覽器兼容框架

    在前端發展的初期,大多數開發最關注的問題就是瀏覽器兼容問題,迫切需要兼容所有瀏覽器的JS和CSS框架。這階段除了橫空出世的jQuery,還有一些其它方面的兼容框架。

     

    1、normalize.css

    讓不同的瀏覽器在渲染網頁元素的時候形式更統一。

     

    2、html5shiv.js

    IE6~IE8識別HTML5標籤,並且可以添加CSS樣式。

     

    3、respond.js

    使IE6~IE8瀏覽器支持媒體查詢。

     

    四、響應式框架

    有了jQuery等兼容框架的基礎,開發人員的關注點,逐漸轉移到越來越豐富的屏幕分辨率上,除開Bootstrap一家獨大,越來越多的響應式框架也在奮起直追。

     

    1、Semantic UI

    https://github.com/semantic-org/semantic-ui

    Semantic 是一個設計漂亮的響應式布局的語義化框架。

     

    2、Bulma

    https://github.com/jgthms/bulma

    基於 Flexbox 的現代 CSS 框架

     

    3、Tailwind

    https://github.com/tailwindcss/tailwindcss

    Tailwind是一個底層CSS 框架,快速 UI 開發的實用工具集,提供了高度可組合的應用程序類,可幫助開發者輕鬆構建複雜的用戶界面。另外Tailwind + Styled Component 簡直是絕配(摘自知乎https://www.zhihu.com/question/337939566)。

     

    4、Materialize

    https://github.com/Dogfalo/materialize

    A CSS Framework based on Material Design.

     

    5、Foundation

    https://github.com/foundation/foundation-sites

    The most advanced responsive front-end framework in the world.

     

    6、Pure.css

    https://github.com/pure-css/pure

    A set of small, responsive CSS modules

     

    7、YAMLCSS

    https://github.com/yamlcss/yaml

    YAML is a modular CSS framework for truly flexible, accessible and responsive websites.

     

    兼容IE6+瀏覽器(能兼容IE6的太稀少了)

     

    五、跨平台框架

    自2009年以來,由於Node.js生態的不斷髮展,前端開發的勢力大漲, AngularJS,BackboneJS,KnockoutJS等一批前端MVC框架開始出現。最終伴隨着React、Angular、Vue等框架的脫穎而出,用前端框架開發移動、桌面應用的野心開始暴漲,開始關注不同平台的差異化,越來越多的跨平台框架開始出現。

     

    1、Framework7

    https://github.com/framework7io/framework7

    Build iOS, Android & Desktop apps

     

     從上圖可以看出,桌面版本比移動版本更緊湊,控件風格跟所在平台近似。支持三種主題:ios、 md、 aurora對應不同平台。

     

    2、Ionic

    https://github.com/ionic-team/ionic

    build mobile and desktop apps

     

     從上圖可以看出,主要針對移動平台優化,但通過API支持多種平台。

     

    3、Onsen UI

    https://github.com/OnsenUI/OnsenUI

    develop HTML5 hybrid and mobile web apps

     

     從上圖可以看出,主要針對移動平台優化,但通過API支持多種平台。

     

    4、Quasar Framework

    https://github.com/quasarframework/quasar

    基於Vue構建響應式網站、PWA、SSR、移動和桌面應用

     

     Quasar將一些輔助CSS類附加到document.body:如desktop、mobile、touch、platform-[ios]、within-iframe等


    5、UNI-APP
     

    https://github.com/dcloudio/uni-app

    使用 Vue.js 開發所有前端應用的框架

     

     從上圖可以看出,三種平台比較一致,但移動版本還比桌面版本還緊湊是什麼意思?

     

    6、橫向對比

    框架

    桌面優化

    移動優化

    移動一致

    支持框架

    Framework7

    優秀

    優秀

    優秀

    最多

    Ionic

    一般

    優秀

    一般

    較多

    Onsen UI

    一般

    優秀

    一般

    較多

    Quasar

    良好

    優秀

    良好

    Vue

    UNI-APP

    一般

    優秀

    優秀

    Vue

     

    六、總結

    兼容性問題總是伴隨着平台的擴張而產生的,Web開發面臨的終極問題就是多平台兼容性問題,根據不同產品,不同階段做部分取捨,應用不同的框架而已。需要支持的平台,決定了你的選擇。

     

    新的框架或舊框架的新版本基本都不再支持IE,但國內還有5.65% 的IE用戶,而且3.29%的WinXP,46.79%的Win7都是潛在的IE用戶,所以可將其做為一個平台看待。

    • IE Web
    • Desktop Web
    • Mobile Web
    • Tablet Web
    • Desktop Hybrid
    • Mobile Hybrid
    • Tablet Hybrid

    注:React Native代表的Native技術不在本次討論之列

     

    1、瀏覽器兼容策略

    國內XP用戶還有3.29%,XP用戶既升級不了IE9,也無法安裝新版本Chrome和Firefox 。而IE用戶還有 5.65%,考慮到Windows用戶為87%,所以IE9+的份額應該要少於5.65%-3.29%*87%=2.79%。也就是說IE8以下的用戶要多於IE8以上的用戶。所以支持單獨支持IE9+ 瀏覽器沒有實際意義,要麼支持IE6,要麼不支持IE,。

     

    看看知名網站對IE8的兼容性,

    • 京東會提示“溫馨提示:您當前的瀏覽器版本過低,存在安全風險,建議升級瀏覽器”,但是頁面完全可以正確显示,幾乎沒有什麼異常發生,看來兼容工作很到位。
    • 淘寶會出現很多頁面異常,說明IE兼容工作要求不高,基本正常即可,只是象徵性的加了幾條兼容性內容。
    • 去哪兒網也會出現很多頁面異常,但頁面布局還是正常的,看來也是儘力而為,不做要求。
    • 騰訊的頁面只有一個立即更新按鈕,一貫地友好。
    • 知乎直接404,好吧,強大。

     

    兼容IE的建議:

    一、建議不做任何兼容,IE6~11直接显示升級瀏覽器按鈕。

    二、如果一定要兼容,後端返回IE專用頁面,至少兼容IE8。

     

    2、屏幕分辨率兼容策略

    屏幕分辨率最少要考慮兼容便攜屏幕和移動屏幕兩種。可以參考去哪兒網的做法,把內容分成三類:移動端主菜單與導航欄;主要內容;擴展內容。屏幕分辨率高於480,显示主要內容、擴展內容。屏幕分辨率低於480,显示移動端主菜單與導航欄、主要內容。

     

    如果你的應用是管理軟件,則最好考慮兼容桌面屏幕、便攜屏幕和移動屏幕三種。Bootstrap5新增了超超大屏幕,則就是基於這種考慮。這時候,可以加入側邊欄自動隱藏/打開,主要內容用Flex方式組織,可以在頁面中並排显示多頁(類似於Word的頁面視圖)。

     

    3、跨平台兼容策略

    大型網站,手機網站與桌面網站是不同的入口,因此不存在兼容,是兩個單獨的應用程序。對於流量較小的網站,平台的兼容策略主要是應用響應式框架,加上移動端主菜單與導航欄即可,其次可以選用跨平台框架來實現在不同平台的差異化體驗。沒有這些框架對於Web網站來說不造成大的體驗下降。而如果需要開發混合移動、桌面應用,則需要認真考慮這些框架,畢竟用戶對本地應用的體驗期待要高很多。

     

     (全文完)

     

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

    【其他文章推薦】

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

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

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

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

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

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

  • .NET 5 嘗鮮 – 開源項目TerminalMACS WPF管理端支持.NET 5

    .NET 5 嘗鮮 – 開源項目TerminalMACS WPF管理端支持.NET 5

    .NET 5 嘗鮮 – 開源項目TerminalMACS WPF管理端支持.NET 5

    一個使用 Prism 作為模塊化框架、基於多個開源控件庫作為UI控件選擇、集成開源 UI 界面設計的 .NET 5 WPF 客戶端項目。

    • 項目名稱:TerminalMACS WPF管理端
    • 項目開源地址:
      • Github:https://github.com/dotnet9/TerminalMACS.ManagerForWPF
      • Gitee:https://gitee.com/dotnet9/TerminalMACS.ManagerForWPF
    • 作者:Dotnet9

    1. 特性

    • 使用 .NET 5 開發,體驗最新 .NET 平台(和 .NET Core 3.1 無縫兼容)

    .NET 5 是 .NET Framework 和 .NET Core 的未來,最終將成為一個統一平台,.NET5將包含ASP.NET核心、實體框架核心、WinForms、WPF、Xamarin 和 ML.NET。

    • 基於 Prism 8 搭建模塊化框架,方便程序擴展

    Prism為程序設計提供指導,旨在幫助用戶更加容易的設計和構建豐富、靈活、易於維護WPF桌面應用程序。Prism使用設計模式(如MVVM,複合視圖,事件聚合器),幫助你創建一個松耦合的程序。遵循這些設計模式原則,將目標程序解耦成獨立的模塊。這些類型的應用程序被稱為複合應用程序。

    • 已使用或即將使用到多個開源WPF控件庫

      • MaterialDesignInXamlToolkit
      • HandyControl
      • PanuonUI.Silver
      • AduSkin。

    參考以上多種開源 WPF UI 庫,多個選擇,開發 WPF 項目更方便。

    • ECharts

    界面設計有使用到ECharts,使用WPF WebBrowser控件加載html的方式

    ECharts:pie-doughnut

    • 本地化支持

    • 動態國際化支持

    • 支持主題色動態切換

    2. 支持環境

    • .NET 5.0。

    3. 當前版本

    0.1

    4. 鏈接

    • 官方網站:Dotnet9

    5. 項目界面截圖

    5.1. 關於

    5.2. 首頁模塊

    正在開發中…

    5.3. 服務端模塊

    正在開發中…

    5.4. 客戶端模塊

    正在開發中…

    5.5. 測試案例

    收集全球優秀的開源WPF界面設計,實時收集、實時添加更新,下面是部分實例截圖:

    登錄註冊分類 1

    1. 簡單登錄窗體設計1

    參考視頻:C# WPF Material Design UI: Login Window

    參考源碼:Login2

    1. 簡單登錄窗體設計2

    參考視頻:C# WPF Material Design UI: Login Window

    參考源碼:Login1

    1. 美食應用登錄

    參考視頻:WPF Food App Login UI Material Design [Speed Design]

    菜單類 2

    1. 抽屜式菜單

    參考視頻:C# WPF Material Design UI: Animated Colorful Navigation Drawer

    參考源碼:AnimatedColorfulMenu

    1. 菜單切換用戶控件

    參考視頻:C# WPF Material Design UI: Fast Food Sales

    參考源碼:Pizzaria1

    1. 菜單切換動畫

    參考視頻:C# WPF Material Design UI: Animated Menu

    參考源碼:AnimatedMenu1

    其他界面設計 3

    1. 移動應用儀錶盤

    參考視頻:WPF Dashboard UI – Material Design [Speed Design]

    參考源碼:WPF-Dashboard-UI-Material-Design-Concept

    1. 簡易儀錶盤2

    參考視頻:WPF Dashboard UI – Material Design [Speed Design]

    參考源碼:WPF-Dashboard-UI-Material-Design-Concept

    ECharts:pie-doughnut

    1. Instagram重新設計

    參考視頻:C# WPF Material Design UI: Redesign Instagram

    參考源碼:Instagram

    1. LoLGoal

    參考視頻:dotnet9

    參考源碼:dotnet9

    1. 簡易音樂播放器1

    參考視頻:C# WPF Material Design UI: Dashboard

    參考源碼:Dashboard

    1. 百度地圖

    通過WPF WebBrowser控件加載html5文件的形式加載百度地圖,使用JavaScript與C#互操作實現地圖交互。

    1. 聊天界面設計

    參考視頻:

    • C# WPF Design UI – 1/3 – Contact List
    • C# WPF Design UI – 2/3 – Profile
    • C# WPF Design UI – 3/3 – Chat

    參考源碼:Chat

    1. 計算器

    參考視頻:

    • Calcalator

    關注Dotnet9,分享更多好文
    如果本文對你有用,歡迎轉載,Dotnet9對應原文有markdown格式原文分享下載哦。

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

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

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

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

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

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

    ※回頭車貨運收費標準

  • Tensorflow2 自定義數據集圖片完成圖片分類任務

    Tensorflow2 自定義數據集圖片完成圖片分類任務

    對於自定義數據集的圖片任務,通用流程一般分為以下幾個步驟:

    • Load data

    • Train-Val-Test

    • Build model

    • Transfer Learning

    其中大部分精力會花在數據的準備和預處理上,本文用一種較為通用的數據處理手段,並通過手動構建,簡單模型, 層數較深的resnet網絡,和基於VGG19的遷移學習。

    你可以通過這個例子,快速搭建網絡,並訓練處一個較為滿意的結果。

    1. Load data

    數據集來自Pokemon的5分類數據, 每一種的圖片數量為200多張,是一個較小型的數據集。

    官方項目鏈接:

    Keras and Convolutional Neural Networks (CNNs)

    1.1 數據集介紹

    Pokemon文件夾中包含5個子文件,其中每個子文件夾名為對應的類別名。文件夾中包含有png, jpeg的圖片文件。

    1.2 解題思路

    • 由於文件夾中沒有劃分,訓練集和測試集,所以需要構建一個csv文件讀取所有的文件,及其類別

    • shuffle數據集以後,劃分Train_val_test

    • 對數據進行預處理, 數據標準化,數據增強, 可視化處理

    “””python
    # 創建数字編碼錶

      import os
      import glob
      import random
      import csv
      import tensorflow as tf
      from tensorflow import keras
      import matplotlib.pyplot as plt
      import time
      
      
      def load_csv(root, filename, name2label):
          """
          將分散在各文件夾中的圖片, 轉換為圖片和label對應的一個dataset文件, 格式為csv
          :param root: 文件路徑(每個子文件夾中的文件屬於一類)
          :param filename: 文件名
          :param name2label: 類名編碼錶  {'類名1':0, '類名2':1..}
          :return: images, labels
          """
          # 判斷是否csv文件已經生成
          if not os.path.exists(os.path.join(root, filename)):  # join-將路徑與文件名何為一個路徑並返回(沒有會生成新路徑)
              images = []  # 存的是文件路徑
              for name in name2label.keys():
                  # pokemon\pikachu\00000001.png
                  # glob.glob() 利用通配符檢索路徑內的文件,類似於正則表達式
                  images += glob.glob(os.path.join(root, name, '*'))  # png, jpg, jpeg
              print(name2label)
              print(len(images), images)
      
              random.shuffle(images)
      
              with open(os.path.join(root, filename), 'w', newline='') as f:
                  writer = csv.writer(f)
                  for img in images:
                      name = img.split(os.sep)[1]  # os.sep 表示分隔符 window-'\\' , linux-'/'
                      label = name2label[name]  # 0, 1, 2..
                      # 'pokemon\\bulbasaur\\00000000.png', 0
                      writer.writerow([img, label])  # 如果不設定newline='', 2個數據會分為2行寫
                  print('write into csv file:', filename)
      
          # 讀取現有文件
          images, labels = [], []
          with open(os.path.join(root, filename)) as f:
              reader = csv.reader(f)
              for row in reader:
                  # 'pokemon\\bulbasaur\\00000000.png', 0
                  img, label = row
                  label = int(label)  # str-> int
                  images.append(img)
                  labels.append(label)
      
          assert len(images) == len(labels)
      
          return images, labels
      
      
      def load_pokemon(root, mode='train'):
          """
          # 創建数字編碼錶
          :param root: root path
          :param mode: train, valid, test
          :return: images, labels, name2label
          """
      
          name2label = {}  # {'bulbasaur': 0, 'charmander': 1, 'mewtwo': 2, 'pikachu': 3, 'squirtle': 4}
          for name in sorted(os.listdir(os.path.join(root))):
              # sorted() 是為了復現結果的一致性
              # os.listdir - 返迴路徑下的所有文件(文件夾,文件)列表
              if not os.path.isdir(os.path.join(root, name)):  # 是否為文件夾且是否存在
                  continue
              # 每個類別編碼一個数字
              name2label[name] = len(name2label)
      
          # 讀取label
          images, labels = load_csv(root, 'images.csv', name2label)
      
          # 劃分數據集 [6:2:2]
          if mode == 'train':
              images = images[:int(0.6 * len(images))]
              labels = labels[:int(0.6 * len(labels))]  # len(images) == len(labels)
      
          elif mode == 'valid':
              images = images[int(0.6 * len(images)):int(0.8 * len(images))]
              labels = labels[int(0.6 * len(labels)):int(0.8 * len(labels))]
      
          else:
              images = images[int(0.8 * len(images)):]
              labels = labels[int(0.8 * len(labels)):]
      
          return images, labels, name2label
      
      
      # imagenet 數據集均值, 方差
      img_mean = tf.constant([0.485, 0.456, 0.406])  # 3 channel
      img_std = tf.constant([0.229, 0.224, 0.225])
      
      def normalization(x, mean=img_mean, std=img_std):
          # [224, 224, 3]
          x = (x - mean) / std
          return x
      
      def denormalization(x, mean=img_mean, std=img_std):
          x = x * std + mean
          return x
      
      
      def preprocess(x, y):
          # x: path, y: label
          x = tf.io.read_file(x)  # 2進制
          # x = tf.image.decode_image(x)
          x = tf.image.decode_jpeg(x, channels=3)  # RGBA
          x = tf.image.resize(x, [244, 244])
      
          # data augmentation
          # x = tf.image.random_flip_up_down(x)
          x = tf.image.random_flip_left_right(x)
          x = tf.image.random_crop(x, [224, 224, 3])  # 模型縮減比例不宜過大,否則會增大訓練難度
      
          x = tf.cast(x, dtype=tf.float32) / 255. # unit8 -> float32
          # U[0,1] -> N(0,1)  # 提高訓練準確度
          x = normalization(x)
      
          y = tf.convert_to_tensor(y)
      
          return x, y
      
      def main():
          images, labels, name2label = load_pokemon('pokemon', 'train')
          print('images:', len(images), images)
          print('labels:', len(labels), labels)
          # print(name2label)
      
          # .map()函數要位於.batch()之前, 否則 x=tf.io.read_file()會一次讀取一個batch的圖片,從而報錯
          db = tf.data.Dataset.from_tensor_slices((images, labels)).map(preprocess).shuffle(1000).batch(32)
      
          # tf.summary()
          # 提供了各類方法(支持各種多種格式)用於保存訓練過程中產生的數據(比如loss_value、accuracy、整個variable),
          # 這些數據以日誌文件的形式保存到指定的文件夾中。
      
          # 數據可視化:而tensorboard可以將tf.summary()
          # 記錄下來的日誌可視化,根據記錄的數據格式,生成折線圖、統計直方圖、圖片列表等多種圖。
          # tf.summary()
          # 通過遞增的方式更新日誌,這讓我們可以邊訓練邊使用tensorboard讀取日誌進行可視化,從而實時監控訓練過程。
          writer = tf.summary.create_file_writer('logs')
          for step, (x, y) in enumerate(db):
              with writer.as_default():
                  x = denormalization(x)
                  tf.summary.image('img', x, step=step, max_outputs=9)  # STEP:默認選項,指的是橫軸显示的是訓練迭代次數
      
                  time.sleep(5)
      
      
      
      if __name__ == '__main__':
          main()
    

    “””

    2. 構建模型進行訓練

    2.1 自定義小型網絡

    由於數據集數量較少,大型網絡的訓練中往往會出現過擬合情況,這裏就定義了一個2層卷積的小型網絡。
    引入early_stopping回調函數后,3個epoch沒有較大變化的情況下,模型訓練的準確率為0.8547

    “””
    # 1. 自定義小型網絡
    model = keras.Sequential([
    layers.Conv2D(16, 5, 3),
    layers.MaxPool2D(3, 3),
    layers.ReLU(),
    layers.Conv2D(64, 5, 3),
    layers.MaxPool2D(2, 2),
    layers.ReLU(),
    layers.Flatten(),
    layers.Dense(64),
    layers.ReLU(),
    layers.Dense(5)
    ])

      model.build(input_shape=(None, 224, 224, 3))  
      model.summary()
      
      early_stopping = EarlyStopping(
          monitor='val_loss',
          patience=3,
          min_delta=0.001
      )
      
      
      model.compile(optimizer=optimizers.Adam(lr=1e-3),
                     loss=losses.CategoricalCrossentropy(from_logits=True),
                     metrics=['accuracy'])
      model.fit(db_train, validation_data=db_val, validation_freq=1, epochs=100,
                 callbacks=[early_stopping])
      model.evaluate(db_test)
    

    “””

    2.2 自定義的Resnet網絡

    resnet 網絡對於層次較深的網絡的可訓練型提升很大,主要是通過一個identity layer保證了深層次網絡的訓練效果不會弱於淺層網絡。
    其他文章中有詳細介紹resnet的搭建,這裏就不做贅述, 這裏構建了一個resnet18網絡, 準確率0.7607。

    “””
    import os

      import numpy as np
      import tensorflow as tf
      from tensorflow import keras
      from tensorflow.keras import layers
      
      tf.random.set_seed(22)
      np.random.seed(22)
      os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
      assert tf.__version__.startswith('2.')
      
      
      class ResnetBlock(keras.Model):
      
          def __init__(self, channels, strides=1):
              super(ResnetBlock, self).__init__()
      
              self.channels = channels
              self.strides = strides
      
              self.conv1 = layers.Conv2D(channels, 3, strides=strides,
                                         padding=[[0, 0], [1, 1], [1, 1], [0, 0]])
              self.bn1 = keras.layers.BatchNormalization()
              self.conv2 = layers.Conv2D(channels, 3, strides=1,
                                         padding=[[0, 0], [1, 1], [1, 1], [0, 0]])
              self.bn2 = keras.layers.BatchNormalization()
      
              if strides != 1:
                  self.down_conv = layers.Conv2D(channels, 1, strides=strides, padding='valid')
                  self.down_bn = tf.keras.layers.BatchNormalization()
      
          def call(self, inputs, training=None):
              residual = inputs
      
              x = self.conv1(inputs)
              x = tf.nn.relu(x)
              x = self.bn1(x, training=training)
              x = self.conv2(x)
              x = tf.nn.relu(x)
              x = self.bn2(x, training=training)
      
              # 殘差連接
              if self.strides != 1:
                  residual = self.down_conv(inputs)
                  residual = tf.nn.relu(residual)
                  residual = self.down_bn(residual, training=training)
      
              x = x + residual
              x = tf.nn.relu(x)
              return x
      
      
      class ResNet(keras.Model):
      
          def __init__(self, num_classes, initial_filters=16, **kwargs):
              super(ResNet, self).__init__(**kwargs)
      
              self.stem = layers.Conv2D(initial_filters, 3, strides=3, padding='valid')
      
              self.blocks = keras.models.Sequential([
                  ResnetBlock(initial_filters * 2, strides=3),
                  ResnetBlock(initial_filters * 2, strides=1),
                  # layers.Dropout(rate=0.5),
      
                  ResnetBlock(initial_filters * 4, strides=3),
                  ResnetBlock(initial_filters * 4, strides=1),
      
                  ResnetBlock(initial_filters * 8, strides=2),
                  ResnetBlock(initial_filters * 8, strides=1),
      
                  ResnetBlock(initial_filters * 16, strides=2),
                  ResnetBlock(initial_filters * 16, strides=1),
              ])
      
              self.final_bn = layers.BatchNormalization()
              self.avg_pool = layers.GlobalMaxPool2D()
              self.fc = layers.Dense(num_classes)
      
          def call(self, inputs, training=None):
              # print('x:',inputs.shape)
              out = self.stem(inputs, training = training)
              out = tf.nn.relu(out)
      
              # print('stem:',out.shape)
      
              out = self.blocks(out, training=training)
              # print('res:',out.shape)
      
              out = self.final_bn(out, training=training)
              # out = tf.nn.relu(out)
      
              out = self.avg_pool(out)
      
              # print('avg_pool:',out.shape)
              out = self.fc(out)
      
              # print('out:',out.shape)
      
              return out
      
      
      def main():
          num_classes = 5
      
          resnet18 = ResNet(5)
          resnet18.build(input_shape=(None, 224, 224, 3))
          resnet18.summary()
      
      
      if __name__ == '__main__':
          main()
    

    “””

    “””
    # 2.resnet18訓練, 圖片數量較小,訓練結果不是特別好
    # resnet = ResNet(5) # 0.7607
    # resnet.build(input_shape=(None, 224, 224, 3))
    # resnet.summary()
    “””

    2.3 VGG19遷移學習

    遷移學習利用了數據集之間的相似性,對於數據集數量較少的時候,訓練效果會遠優於其他。
    在訓練過程中,使用include_top=False, 去掉最後分類的基層Dense, 重新構建並訓練就可以了。準確率0.9316

    “””
    # 3. VGG19遷移學習,遷移學習利用數據集之間的相似性, 結果遠好於其他2種
    # 為了方便,這裏仍然使用resnet命名
    net = tf.keras.applications.VGG19(weights=’imagenet’, include_top=False, pooling=’max’ )
    net.trainable = False
    resnet = keras.Sequential([
    net,
    layers.Dense(5)
    ])
    resnet.build(input_shape=(None, 224, 224, 3)) # 0.9316
    resnet.summary()

      early_stopping = EarlyStopping(
          monitor='val_loss',
          patience=3,
          min_delta=0.001
      )
      
      
      resnet.compile(optimizer=optimizers.Adam(lr=1e-3),
                     loss=losses.CategoricalCrossentropy(from_logits=True),
                     metrics=['accuracy'])
      resnet.fit(db_train, validation_data=db_val, validation_freq=1, epochs=100,
                 callbacks=[early_stopping])
      resnet.evaluate(db_test)
    

    “””

    附錄:

    train_scratch.py 代碼

    “””

    import os
    
    os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
    
    import tensorflow as tf
    import numpy as np
    from tensorflow import keras
    from tensorflow.keras import layers, optimizers, losses
    from tensorflow.keras.callbacks import EarlyStopping
    
    tf.random.set_seed(22)
    np.random.seed(22)
    assert tf.__version__.startswith('2.')
    
    # 設置GPU顯存按需分配
    # gpus = tf.config.experimental.list_physical_devices('GPU')
    # if gpus:
    #     try:
    #         # Currently, memory growth needs to be the same across GPUs
    #         for gpu in gpus:
    #             tf.config.experimental.set_memory_growth(gpu, True)
    #         logical_gpus = tf.config.experimental.list_logical_devices('GPU')
    #         print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs")
    #     except RuntimeError as e:
    #         # Memory growth must be set before GPUs have been initialized
    #         print(e)
    
    from pokemon import load_pokemon, normalization
    from resnet import ResNet
    
    
    def preprocess(x, y):
        # x: 圖片的路徑,y:圖片的数字編碼
        x = tf.io.read_file(x)
        x = tf.image.decode_jpeg(x, channels=3)  # RGBA
        # 圖片縮放
        # x = tf.image.resize(x, [244, 244])
        # 圖片旋轉
        # x = tf.image.rot90(x,2)
        # 隨機水平翻轉
        x = tf.image.random_flip_left_right(x)
        # 隨機豎直翻轉
        # x = tf.image.random_flip_up_down(x)
    
        # 圖片先縮放到稍大尺寸
        x = tf.image.resize(x, [244, 244])
        # 再隨機裁剪到合適尺寸
        x = tf.image.random_crop(x, [224, 224, 3])
    
        # x: [0,255]=> -1~1
        x = tf.cast(x, dtype=tf.float32) / 255.
        x = normalization(x)
        y = tf.convert_to_tensor(y)
        y = tf.one_hot(y, depth=5)
    
        return x, y
    
    
    batchsz = 32
    
    # create train db
    images1, labels1, table = load_pokemon('pokemon', 'train')
    db_train = tf.data.Dataset.from_tensor_slices((images1, labels1))
    db_train = db_train.shuffle(1000).map(preprocess).batch(batchsz)
    # create validation db
    images2, labels2, table = load_pokemon('pokemon', 'valid')
    db_val = tf.data.Dataset.from_tensor_slices((images2, labels2))
    db_val = db_val.map(preprocess).batch(batchsz)
    # create test db
    images3, labels3, table = load_pokemon('pokemon', mode='test')
    db_test = tf.data.Dataset.from_tensor_slices((images3, labels3))
    db_test = db_test.map(preprocess).batch(batchsz)
    
    
    # 1. 自定義小型網絡
    # resnet = keras.Sequential([
    #     layers.Conv2D(16, 5, 3),
    #     layers.MaxPool2D(3, 3),
    #     layers.ReLU(),
    #     layers.Conv2D(64, 5, 3),
    #     layers.MaxPool2D(2, 2),
    #     layers.ReLU(),
    #     layers.Flatten(),
    #     layers.Dense(64),
    #     layers.ReLU(),
    #     layers.Dense(5)
    # ])  # 0.8547
    
    
    # 2.resnet18訓練, 圖片數量較小,訓練結果不是特別好
    # resnet = ResNet(5)  # 0.7607
    # resnet.build(input_shape=(None, 224, 224, 3))
    # resnet.summary()
    
    
    # 3. VGG19遷移學習,遷移學習利用數據集之間的相似性, 結果遠好於其他2種
    net = tf.keras.applications.VGG19(weights='imagenet', include_top=False, pooling='max' )
    net.trainable = False
    resnet = keras.Sequential([
        net,
        layers.Dense(5)
    ])
    resnet.build(input_shape=(None, 224, 224, 3))   # 0.9316
    resnet.summary()
    
    early_stopping = EarlyStopping(
        monitor='val_loss',
        patience=3,
        min_delta=0.001
    )
    
    
    resnet.compile(optimizer=optimizers.Adam(lr=1e-3),
                   loss=losses.CategoricalCrossentropy(from_logits=True),
                   metrics=['accuracy'])
    resnet.fit(db_train, validation_data=db_val, validation_freq=1, epochs=100,
               callbacks=[early_stopping])
    resnet.evaluate(db_test)
    

    “””

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

    【其他文章推薦】

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

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

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

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

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

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

  • 這篇文章,我們來談一談Spring中的屬性注入

    這篇文章,我們來談一談Spring中的屬性注入

    本系列文章:

    讀源碼,我們可以從第一行讀起

    你知道Spring是怎麼解析配置類的嗎?

    配置類為什麼要添加@Configuration註解?

    談談Spring中的對象跟Bean,你知道Spring怎麼創建對象的嗎?

    推薦閱讀:

    Spring官網閱讀 | 總結篇

    Spring雜談

    本系列文章將會帶你一行行的將Spring的源碼吃透,推薦閱讀的文章是閱讀源碼的基礎!

    前言

    在前面的文章中已經知道了Spring是如何將一個對象創建出來的,那麼緊接着,Spring就需要將這個對象變成一個真正的Bean了,這個過程主要分為兩步

    1. 屬性注入
    2. 初始化

    在這兩個過程中,Bean的後置處理器會穿插執行,其中有些後置處理器是為了幫助完成屬性注入或者初始化的,而有些後置處理器是Spring提供給程序員進行擴展的,當然,這二者並不衝突。整個Spring創建對象並將對象變成Bean的過程就是我們經常提到了Spring中Bean的生命周期。當然,本系列源碼分析的文章不會再對生命周期的概念做過多闡述了,如果大家有這方面的需求的話可以參考我之前的文章,或者關注我的公眾號:程序員DMZ

    Spring官網閱讀(九)Spring中Bean的生命周期(上)

    Spring官網閱讀(十)Spring中Bean的生命周期(下)

    源碼分析

    閑話不再多說,我們正式進入源碼分析階段,本文重點要分析的方法就是org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean,其源碼如下:

    doCreateBean

    	protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
    			throws BeanCreationException {
    
    		// 創建對象的過程在上篇文章中我們已經介紹過了,這裏不再贅述
    		BeanWrapper instanceWrapper = null;
    		if (mbd.isSingleton()) {
    			instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
    		}
    		if (instanceWrapper == null) {
    			instanceWrapper = createBeanInstance(beanName, mbd, args);
    		}
            
            // 獲取到創建的這個對象
    		final Object bean = instanceWrapper.getWrappedInstance();
    		Class<?> beanType = instanceWrapper.getWrappedClass();
    		if (beanType != NullBean.class) {
    			mbd.resolvedTargetType = beanType;
    		}
    
    		// Allow post-processors to modify the merged bean definition.
            // 按照官方的註釋來說,這個地方是Spring提供的一個擴展點,對程序員而言,我們可以通過一個實現了MergedBeanDefinitionPostProcessor的後置處理器來修改bd中的屬性,從而影響到後續的Bean的生命周期
            // 不過官方自己實現的後置處理器並沒有去修改bd,而是調用了applyMergedBeanDefinitionPostProcessors方法
            // 這個方法名直譯過來就是-應用合併后的bd,也就是說它這裏只是對bd做了進一步的使用而沒有真正的修改
    		synchronized (mbd.postProcessingLock) {
               // bd只允許被處理一次
    			if (!mbd.postProcessed) {
    				try {
                        // 應用合併后的bd
    					applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
    				}
    				catch (Throwable ex) {
    					throw new BeanCreationException(mbd.getResourceDescription(), beanName,
    							"Post-processing of merged bean definition failed", ex);
    				}
                    // 標註這個bd已經被MergedBeanDefinitionPostProcessor的後置處理器處理過
                    // 那麼在第二次創建Bean的時候,不會再次調用applyMergedBeanDefinitionPostProcessors
    				mbd.postProcessed = true;
    			}
    		}
    
    		// 這裡是用來出來循環依賴的,關於循環以來,在介紹完正常的Bean的創建后,單獨用一篇文章說明
            // 這裏不做過多解釋
    		boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
    				isSingletonCurrentlyInCreation(beanName));
    		if (earlySingletonExposure) {
    			if (logger.isTraceEnabled()) {
    				logger.trace("Eagerly caching bean '" + beanName +
    						"' to allow for resolving potential circular references");
    			}
    			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    		}
    
    
    		Object exposedObject = bean;
    		try {
                // 我們這篇文章重點要分析的就是populateBean方法,在這個方法中完成了屬性注入
    			populateBean(beanName, mbd, instanceWrapper);
                // 初始化
    			exposedObject = initializeBean(beanName, exposedObject, mbd);
    		}
    		catch (Throwable ex) {
    			// 省略異常代碼
    		}
    
    		// 後續代碼不在本文探討範圍內了,暫不考慮
    
    		return exposedObject;
    	}
    

    applyMergedBeanDefinitionPostProcessors

    源碼如下:

    // 可以看到這個方法的代碼還是很簡單的,就是調用了MergedBeanDefinitionPostProcessor的postProcessMergedBeanDefinition方法
    protected void applyMergedBeanDefinitionPostProcessors(RootBeanDefinition mbd, Class<?> beanType, String beanName) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof MergedBeanDefinitionPostProcessor) {
                MergedBeanDefinitionPostProcessor bdp = (MergedBeanDefinitionPostProcessor) bp;
                bdp.postProcessMergedBeanDefinition(mbd, beanType, beanName);
            }
        }
    }
    

    這個時候我們就要思考一個問題,容器中現在有哪些後置處理器是MergedBeanDefinitionPostProcessor呢?

    查看這個方法的實現類我們會發現總共就這麼幾個類實現了MergedBeanDefinitionPostProcessor接口。實際上除了ApplicationListenerDetector之外,其餘的後置處理器的邏輯都差不多。我們在這裏我們主要就分析兩個後置處理

    1. ApplicationListenerDetector
    2. AutowiredAnnotationBeanPostProcessor

    ApplicationListenerDetector

    首先,我們來ApplicationListenerDetector,這個類在之前的文章中也多次提到過了,它的作用是用來處理嵌套Bean的情況,主要是保證能將嵌套在Bean標籤中的ApplicationListener也能添加到容器的監聽器集合中去。我們先通過一個例子來感受下這個後置處理器的作用吧

    配置文件:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    	<bean class="com.dmz.source.populate.service.DmzService" id="dmzService">
    		<constructor-arg name="orderService">
    			<bean class="com.dmz.source.populate.service.OrderService"/>
    		</constructor-arg>
    	</bean>
    </beans>
    

    示例代碼:

    // 事件
    public class DmzEvent extends ApplicationEvent {
    	public DmzEvent(Object source) {
    		super(source);
    	}
    }
    
    public class DmzService {
    
    	OrderService orderService;
    
    	public DmzService(OrderService orderService) {
    		this.orderService = orderService;
    	}
    }
    // 實現ApplicationListener接口
    public class OrderService implements ApplicationListener<DmzEvent> {
    	@Override
    	public void onApplicationEvent(DmzEvent event) {
    		System.out.println(event.getSource());
    	}
    }
    
    public class Main {
    	public static void main(String[] args) {
    		ClassPathXmlApplicationContext cc = new ClassPathXmlApplicationContext("application-populate.xml");
    		cc.publishEvent(new DmzEvent("my name is dmz"));
    	}
    }
    
    // 程序運行結果,控制台打印:my name is dmz
    

    說明OrderService已經被添加到了容器的監聽器集合中。但是請注意,在這種情況下,如果要使OrderService能夠執行監聽的邏輯,必須要滿足下面這兩個條件

    • 外部的Bean要是單例的,對於我們的例子而言就是dmzService
    • 內嵌的Bean也必須是單例的,在上面的例子中也就是orderService必須是單例

    另外需要注意的是,這種嵌套的Bean比較特殊,它雖然由Spring創建,但是確不存在於容器中,就是說我們不能將其作為依賴注入到別的Bean中。

    AutowiredAnnotationBeanPostProcessor

    對應源碼如下:

    public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
        // 找到注入的元數據,第一次是構建,後續可以直接從緩存中拿
        // 註解元數據其實就是當前這個類中的所有需要進行注入的“點”的集合,
        // 注入點(InjectedElement)包含兩種,字段/方法
        // 對應的就是AutowiredFieldElement/AutowiredMethodElement
        InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
        // 排除掉被外部管理的注入點
        metadata.checkConfigMembers(beanDefinition);
    }
    

    上面代碼的核心邏輯就是

    • 找到所有的注入點,其實就是被@Autowired註解修飾的方法以及字段,同時靜態的方法以及字段也會被排除
    • 排除掉被外部管理的注入點,在後續的源碼分析中我們再細說

    findAutowiringMetadata

    // 這個方法的核心邏輯就是先從緩存中獲取已經解析好的注入點信息,很明顯,在原型情況下才會使用緩存
    // 創建注入點的核心邏輯在buildAutowiringMetadata方法中
    private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
        String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
        InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
        // 可能我們會修改bd中的class屬性,那麼InjectionMetadata中的注入點信息也需要刷新
        if (InjectionMetadata.needsRefresh(metadata, clazz)) {
            synchronized (this.injectionMetadataCache) {
                metadata = this.injectionMetadataCache.get(cacheKey);
                if (InjectionMetadata.needsRefresh(metadata, clazz)) {
                    if (metadata != null) {
                        metadata.clear(pvs);
                    }
                    // 這裏真正創建注入點
                    metadata = buildAutowiringMetadata(clazz);
                    this.injectionMetadataCache.put(cacheKey, metadata);
                }
            }
        }
        return metadata;
    }
    

    buildAutowiringMetadata

    // 我們應用中使用@Autowired註解標註在字段上或者setter方法能夠完成屬性注入
    // 就是因為這個方法將@Autowired註解標註的方法以及字段封裝成InjectionMetadata
    // 在後續階段會調用InjectionMetadata的inject方法進行注入
    private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
        List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
        Class<?> targetClass = clazz;
    
        do {
            final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
    		// 處理所有的被@AutoWired/@Value註解標註的字段
            ReflectionUtils.doWithLocalFields(targetClass, field -> {
                AnnotationAttributes ann = findAutowiredAnnotation(field);
                if (ann != null) {
                    // 靜態字段會直接跳過
                    if (Modifier.isStatic(field.getModifiers())) {
                        // 省略日誌打印
                        return;
                    }
                    // 得到@AutoWired註解中的required屬性
                    boolean required = determineRequiredStatus(ann);
                    currElements.add(new AutowiredFieldElement(field, required));
                }
            });
    		// 處理所有的被@AutoWired註解標註的方法,相對於字段而言,這裏需要對橋接方法進行特殊處理
            ReflectionUtils.doWithLocalMethods(targetClass, method -> {
                // 只處理一種特殊的橋接場景,其餘的橋接方法都會被忽略
                Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
                if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
                    return;
                }
                AnnotationAttributes ann = findAutowiredAnnotation(bridgedMethod);
                // 處理方法時需要注意,當父類中的方法被子類重寫時,如果子父類中的方法都加了@Autowired
                // 那麼此時父類方法不能被處理,即不能被封裝成一個AutowiredMethodElement
                if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
                    if (Modifier.isStatic(method.getModifiers())) {
                        // 省略日誌打印
                        return;
                    }
                    if (method.getParameterCount() == 0) {
                        // 當方法的參數數量為0時,雖然不需要進行注入,但是還是會把這個方法作為注入點使用
                        // 這個方法最終還是會被調用
                        if (logger.isInfoEnabled()) {
                            logger.info("Autowired annotation should only be used on methods with parameters: " +
                                        method);
                        }
                    }
                    boolean required = determineRequiredStatus(ann);
                    // PropertyDescriptor: 屬性描述符
                    // 就是通過解析getter/setter方法,例如void getA()會解析得到一個屬性名稱為a
                    // readMethod為getA的PropertyDescriptor,
                    // 在《Spring官網閱讀(十四)Spring中的BeanWrapper及類型轉換》文中已經做過解釋
                    // 這裏不再贅述,這裏之所以來這麼一次查找是因為當XML中對這個屬性進行了配置后,
                    // 那麼就不會進行自動注入了,XML中显示指定的屬性優先級高於註解
                    PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);		   // 構造一個對應的AutowiredMethodElement,後續這個方法會被執行
                    // 方法的參數會被自動注入,這裏不限於setter方法
                    currElements.add(new AutowiredMethodElement(method, required, pd));
                }
            });
    		// 會處理父類中字段上及方法上的@AutoWired註解,並且父類的優先級比子類高
            elements.addAll(0, currElements);
            targetClass = targetClass.getSuperclass();
        }
        while (targetClass != null && targetClass != Object.class);
    
        return new InjectionMetadata(clazz, elements);
    }
    
    難點代碼分析

    上面的代碼整體來說應該很簡單,就如我們之前所說的,處理帶有@Autowired註解的字段及方法,同時會過濾掉所有的靜態字段及方法。上面複雜的地方在於對橋接方法的處理,可能大部分人都沒辦法理解這幾行代碼:

    // 第一行
    Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
    
    // 第二行
    if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
        return;
    }
    
    // 第三行
    if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
    
    }
    

    要理解這些代碼,首先你得知道什麼是橋接,為此我已經寫好了一篇文章:

    Spring雜談 | 從橋接方法到JVM方法調用

    除了在上面的文章中提到的橋接方法外,還有一種特殊的情況

    // A類跟B類在同一個包下,A不是public的
    class A {
    	public void test(){
    
    	}
    }
    
    // 在B中會生成一個跟A中的方法描述符(參數+返回值)一模一樣的橋接方法
    // 這個橋接方法實際上就是調用父類中的方法
    // 具體可以參考:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=63424113
    public class B extends A {
    }
    

    在理解了什麼是橋接之後,那麼上邊的第一行代碼你應該就能看懂了,就以上面的代碼為例,B中會生成一個橋接方法,對應的被橋接的方法就是A中的test方法。

    接着,我們看看第二行代碼

    public static boolean isVisibilityBridgeMethodPair(Method bridgeMethod, Method bridgedMethod) {
        // 說明這個方法本身就不是橋接方法,直接返回true
        if (bridgeMethod == bridgedMethod) {
            return true;
        }
        // 說明是橋接方法,並且方法描述符一致
        // 當且僅當是上面例子中描述的這種橋接的時候這個判斷才會滿足
        // 正常來說橋接方法跟被橋接方法的返回值+參數類型肯定不一致
        // 所以這個判斷會過濾掉其餘的所有類型的橋接方法
        // 只會保留本文提及這種特殊情況下產生的橋接方法
        return (bridgeMethod.getReturnType().equals(bridgedMethod.getReturnType()) &&
                Arrays.equals(bridgeMethod.getParameterTypes(), bridgedMethod.getParameterTypes()));
    }
    

    最後,再來看看第三行代碼,核心就是這句 method.equals(ClassUtils.getMostSpecificMethod(method, clazz)。這句代碼的主要目的就是為了處理下面這種情況

    @Component
    public class D extends C {
    
    	@Autowired
    	@Override
    	public void setDmzService(DmzService dmzService) {
    		dmzService.init();
    		this.dmzService = dmzService;
    	}
    }
    
    // C不是Spring中的組件
    public class C {
    	DmzService dmzService;
        @Autowired
    	public void setDmzService(DmzService dmzService) {
    		this.dmzService = dmzService;
    	}
    }
    
    

    這種情況下,在處理D中的@Autowired註解時,雖然我們要處理父類中的@Autowired註解,但是因為子類中的方法已經複寫了父類中的方法,所以此時應該要跳過父類中的這個被複寫的方法,這就是第三行代碼的作用。

    小結

    到這裏我們主要分析了applyMergedBeanDefinitionPostProcessors這段代碼的作用,它的執行時機是在創建對象之後,屬性注入之前。按照官方的定義來說,到這裏我們仍然可以使用這個方法來修改bd的定義,那麼相對於通過BeanFactoryPostProcessor的方式修改bd,applyMergedBeanDefinitionPostProcessors這個方法影響的範圍更小,BeanFactoryPostProcessor影響的是整個Bean的生命周期,而applyMergedBeanDefinitionPostProcessors只會影響屬性注入之後的生命周期。

    其次,我們分析了Spring中內置的MergedBeanDefinitionPostProcessor,選取了其中兩個特殊的後置處理器進行分析,其中ApplicationListenerDetector主要處理內嵌的事件監聽器,而AutowiredAnnotationBeanPostProcessor主要用於處理@Autowired註解,實際上我們會發現,到這裏還只是完成了@Autowired註解的解析,還沒有真正開始進行注入,真正注入的邏輯在後面我們要分析的populateBean方法中,在這個方法中會使用解析好的注入元信息完成真正的屬性注入,那麼接下來我們就開始分析populateBean這個方法的源碼。

    populateBean

    循環依賴的代碼我們暫且跳過,後續出一篇專門文章解讀循環依賴,我們直接看看populateBean到底做了什麼。

    protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
    
        // 處理空實例
        if (bw == null) {
            // 如果創建的對象為空,但是在XML中又配置了需要注入的屬性的話,那麼直接報錯
            if (mbd.hasPropertyValues()) {
                throw new BeanCreationException(
                    mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance");
            }
            else {
                // 空對象,不進行屬性注入
                return;
            }
        }
    
        // 滿足兩個條件,不是合成類 && 存在InstantiationAwareBeanPostProcessor
        // 其中InstantiationAwareBeanPostProcessor主要作用就是作為Bean的實例化前後的鈎子
        // 外加完成屬性注入,對於三個方法就是
        // postProcessBeforeInstantiation  創建對象前調用
        // postProcessAfterInstantiation   對象創建完成,@AutoWired註解解析后調用   
        // postProcessPropertyValues(已過期,被postProcessProperties替代) 進行屬性注入
        // 下面這段代碼的主要作用就是我們可以提供一個InstantiationAwareBeanPostProcessor
        // 提供的這個後置處理如果實現了postProcessAfterInstantiation方法並且返回false
        // 那麼可以跳過Spring默認的屬性注入,但是這也意味着我們要自己去實現屬性注入的邏輯
        // 所以一般情況下,我們也不會這麼去擴展
        if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
            for (BeanPostProcessor bp : getBeanPostProcessors()) {
                if (bp instanceof InstantiationAwareBeanPostProcessor) {
                    InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
                    if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
                        return;
                    }
                }
            }
        }
    	
        // 這裏其實就是判斷XML是否提供了屬性相關配置
        PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);
    	
        // 確認注入模型
        int resolvedAutowireMode = mbd.getResolvedAutowireMode();
        
        // 主要處理byName跟byType兩種注入模型,byConstructor這種注入模型在創建對象的時候已經處理過了
        // 這裏都是對自動注入進行處理,byName跟byType兩種注入模型均是依賴setter方法
        // byName,根據setter方法的名字來查找對應的依賴,例如setA,那麼就是去容器中查找名字為a的Bean
        // byType,根據setter方法的參數類型來查找對應的依賴,例如setXx(A a),就是去容器中查詢類型為A的bean
        if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
            MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
            if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
                autowireByName(beanName, mbd, bw, newPvs);
            }
            if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
                autowireByType(beanName, mbd, bw, newPvs);
            }
            // pvs是XML定義的屬性
            // 自動注入后,bean實際用到的屬性就應該要替換成自動注入后的屬性
            pvs = newPvs;
        }
    	// 檢查是否有InstantiationAwareBeanPostProcessor
        // 前面說過了,這個後置處理器就是來完成屬性注入的
        boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
        
        //  是否需要依賴檢查,默認是不會進行依賴檢查的
        boolean needsDepCheck = (mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE);
    	
        // 下面這段代碼有點麻煩了,因為涉及到版本問題
        // 其核心代碼就是調用了postProcessProperties完成了屬性注入
       
        PropertyDescriptor[] filteredPds = null;
        
        // 存在InstantiationAwareBeanPostProcessor,我們需要調用這類後置處理器的方法進行注入
    		if (hasInstAwareBpps) {
    			if (pvs == null) {
    				pvs = mbd.getPropertyValues();
    			}
    			for (BeanPostProcessor bp : getBeanPostProcessors()) {
    				if (bp instanceof InstantiationAwareBeanPostProcessor) {
    					InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
                        // 這句就是核心
    					PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
    					if (pvsToUse == null) {
    						if (filteredPds == null) {
                                // 得到需要進行依賴檢查的屬性的集合
    							filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
    						}
                            //  這個方法已經過時了,放到這裏就是為了兼容老版本
    						pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
    						if (pvsToUse == null) {
    							return;
    						}
    					}
    					pvs = pvsToUse;
    				}
    			}
    		}
        // 需要進行依賴檢查
    		if (needsDepCheck) {
    			if (filteredPds == null) {
                    // 得到需要進行依賴檢查的屬性的集合
    				filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
    			}
                // 對需要進行依賴檢查的屬性進行依賴檢查
    			checkDependencies(beanName, mbd, filteredPds, pvs);
    		}
        // 將XML中的配置屬性應用到Bean上
    		if (pvs != null) {
    			applyPropertyValues(beanName, mbd, bw, pvs);
    		}
    }
    

    上面這段代碼主要可以拆分為三個部分

    1. 處理自動注入
    2. 處理屬性注入(主要指處理@Autowired註解),最重要
    3. 處理依賴檢查

    處理自動注入

    autowireByName

    對應源碼如下:

    protected void autowireByName(
        String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
        // 得到符合下麵條件的屬性名稱
        // 1.有setter方法
        // 2.需要進行依賴檢查
        // 3.不包含在XML配置中
        // 4.不是簡單類型(基本數據類型,枚舉,日期等)
        // 這裏可以看到XML配置優先級高於自動注入的優先級
        // 不進行依賴檢查的屬性,也不會進行屬性注入
        String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
        for (String propertyName : propertyNames) {
            if (containsBean(propertyName)) {
                Object bean = getBean(propertyName);
                // 將自動注入的屬性添加到pvs中去
                pvs.add(propertyName, bean);
                // 註冊bean之間的依賴關係
                registerDependentBean(propertyName, beanName);
                // 忽略日誌
            }
            // 忽略日誌
        }
    }
    

    看到了嗎?代碼就是這麼的簡單,不是要通過名稱注入嗎?直接通過beanName調用getBean,完事兒

    autowireByType

    	protected void autowireByType(
    			String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
    		// 這個類型轉換器,主要是在處理@Value時需要使用
    		TypeConverter converter = getCustomTypeConverter();
    		if (converter == null) {
    			converter = bw;
    		}
    
    		Set<String> autowiredBeanNames = new LinkedHashSet<>(4);
    		// 得到符合下麵條件的屬性名稱
    		// 1.有setter方法
    		// 2.需要進行依賴檢查
    		// 3.不包含在XML配置中
    		// 4.不是簡單類型(基本數據類型,枚舉,日期等)
    		// 這裏可以看到XML配置優先級高於自動注入的優先級
    		String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
    		for (String propertyName : propertyNames) {
    			try {
    				PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName);
    				if (Object.class != pd.getPropertyType()) {
    					// 這裏獲取到的就是setter方法的參數,因為我們需要按照類型進行注入嘛
    					MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd);
    					
                        // 如果是PriorityOrdered在進行類型匹配時不會去匹配factoryBean
    					// 如果不是PriorityOrdered,那麼在查找對應類型的依賴的時候會會去匹factoryBean
    				 	// 這就是Spring的一種設計理念,實現了PriorityOrdered接口的Bean被認為是一種
                        // 最高優先級的Bean,這一類的Bean在進行為了完成裝配而去檢查類型時,
                        // 不去檢查factoryBean
                        // 具體可以參考PriorityOrdered接口上的註釋文檔
    					boolean eager = !(bw.getWrappedInstance() instanceof PriorityOrdered);
    					// 將參數封裝成為一個依賴描述符
    					// 依賴描述符會通過:依賴所在的類,字段名/方法名,依賴的具體類型等來描述這個依賴
    					DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager);
    					// 解析依賴,這裡會處理@Value註解
                        // 另外,通過指定的類型到容器中查找對應的bean
    					Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter);
    					if (autowiredArgument != null) {
    						// 將查找出來的依賴屬性添加到pvs中,後面會將這個pvs應用到bean上
    						pvs.add(propertyName, autowiredArgument);
    					}
    					// 註冊bean直接的依賴關係
    					for (String autowiredBeanName : autowiredBeanNames) {
    						registerDependentBean(autowiredBeanName, beanName);
    						if (logger.isDebugEnabled()) {
    							logger.debug("Autowiring by type from bean name '" + beanName + "' via property '" +
    									propertyName + "' to bean named '" + autowiredBeanName + "'");
    						}
    					}
    					autowiredBeanNames.clear();
    				}
    			}
    			catch (BeansException ex) {
    				throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, propertyName, ex);
    			}
    		}
    	}
    
    
    resolveDependency

    這個方法在Spring雜談 | 什麼是ObjectFactory?什麼是ObjectProvider?已經做過分析了,本文不再贅述。

    可以看到,真正做事的方法是doResolveDependency

    @Override
    public Object resolveDependency(DependencyDescriptor descriptor, String requestingBeanName, Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
    	// descriptor代表當前需要注入的那個字段,或者方法的參數,也就是注入點
        // ParameterNameDiscovery用於解析方法參數名稱
        descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
        // 1. Optional<T>
        if (Optional.class == descriptor.getDependencyType()) {
            return createOptionalDependency(descriptor, requestingBeanName);
        // 2. ObjectFactory<T>、ObjectProvider<T>
        } else if (ObjectFactory.class == descriptor.getDependencyType() ||
                 ObjectProvider.class == descriptor.getDependencyType()) {
            return new DependencyObjectProvider(descriptor, requestingBeanName);
        // 3. javax.inject.Provider<T>
        } else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
            return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName);
        } else {
            // 4. @Lazy
            Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
                descriptor, requestingBeanName);
            // 5. 正常情況
            if (result == null) {
                result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
            }
            return result;
        }
    }
    
    doResolveDependency
    	public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
    			@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
    
    		InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
    		try {
    			Object shortcut = descriptor.resolveShortcut(this);
    			if (shortcut != null) {
    				return shortcut;
    			}
    			// 依賴的具體類型
    			Class<?> type = descriptor.getDependencyType();
    			// 處理@Value註解,這裏得到的時候@Value中的值
    			Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
    			if (value != null) {
    				if (value instanceof String) {
    					// 解析@Value中的佔位符
    					String strVal = resolveEmbeddedValue((String) value);
    					// 獲取到對應的bd
    					BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null);
    					// 處理EL表達式
    					value = evaluateBeanDefinitionString(strVal, bd);
    				}
    				// 通過解析el表達式可能還需要進行類型轉換
    				TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
    				return (descriptor.getField() != null ?
    						converter.convertIfNecessary(value, type, descriptor.getField()) :
    						converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
    			}
    			
                // 對map,collection,數組類型的依賴進行處理
    			// 最終會根據集合中的元素類型,調用findAutowireCandidates方法
    			Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
    			if (multipleBeans != null) {
    				return multipleBeans;
    			}
    			
                // 根據指定類型可能會找到多個bean
                // 這裏返回的既有可能是對象,也有可能是對象的類型
                // 這是因為到這裏還不能明確的確定當前bean到底依賴的是哪一個bean
                // 所以如果只會返回這個依賴的類型以及對應名稱,最後還需要調用getBean(beanName)
                // 去創建這個Bean
    			Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
    			// 一個都沒找到,直接拋出異常
    			if (matchingBeans.isEmpty()) {
    				if (isRequired(descriptor)) {
    					raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
    				}
    				return null;
    			}
    
    			String autowiredBeanName;
    			Object instanceCandidate;
    			// 通過類型找到了多個
    			if (matchingBeans.size() > 1) {
    				// 根據是否是主Bean
    				// 是否是最高優先級的Bean
    				// 是否是名稱匹配的Bean
    				// 來確定具體的需要注入的Bean的名稱
                    // 到這裏可以知道,Spring在查找依賴的時候遵循先類型再名稱的原則(沒有@Qualifier註解情況下)
    				autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
    				if (autowiredBeanName == null) {
    					// 無法推斷出具體的名稱
    					// 如果依賴是必須的,直接拋出異常
    					// 如果依賴不是必須的,但是這個依賴類型不是集合或者數組,那麼也拋出異常
    					if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
    						return descriptor.resolveNotUnique(type, matchingBeans);
    					}
    					// 依賴不是必須的,但是依賴類型是集合或者數組,那麼返回一個null
    					else {
    						return null;
    					}
    				}
    				instanceCandidate = matchingBeans.get(autowiredBeanName);
    			}
    			else {
    				// 直接找到了一個對應的Bean
    				Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
    				autowiredBeanName = entry.getKey();
    				instanceCandidate = entry.getValue();
    			}
    			if (autowiredBeanNames != null) {
    				autowiredBeanNames.add(autowiredBeanName);
    			}
                
                // 前面已經說過了,這裏可能返回的是Bean的類型,所以需要進一步調用getBean
    			if (instanceCandidate instanceof Class) {
    				instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);
    			}
                
                // 做一些檢查,如果依賴是必須的,查找出來的依賴是一個null,那麼報錯
                // 查詢處理的依賴類型不符合,也報錯
    			Object result = instanceCandidate;
    			if (result instanceof NullBean) {
    				if (isRequired(descriptor)) {
    					raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
    				}
    				result = null;
    			}
    			if (!ClassUtils.isAssignableValue(type, result)) {
    				throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass());
    			}
    			return result;
    		}
    		finally {
    			ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
    		}
    	}
    
    findAutowireCandidates
    protected Map<String, Object> findAutowireCandidates(
        @Nullable String beanName, Class<?> requiredType, DependencyDescriptor descriptor) {
    	
        // 簡單來說,這裏就是到容器中查詢requiredType類型的所有bean的名稱的集合
        // 這裡會根據descriptor.isEager()來決定是否要匹配factoryBean類型的Bean
        // 如果isEager()為true,那麼會匹配factoryBean,反之,不會
        String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
            this, requiredType, true, descriptor.isEager());
       
        Map<String, Object> result = new LinkedHashMap<>(candidateNames.length);
       
        // 第一步會到resolvableDependencies這個集合中查詢是否已經存在了解析好的依賴
        // 像我們之所以能夠直接在Bean中注入applicationContext對象
        // 就是因為Spring之前就將這個對象放入了resolvableDependencies集合中
        for (Class<?> autowiringType : this.resolvableDependencies.keySet()) {
            if (autowiringType.isAssignableFrom(requiredType)) {
                Object autowiringValue = this.resolvableDependencies.get(autowiringType);
                
                // 如果resolvableDependencies放入的是一個ObjectFactory類型的依賴
                // 那麼在這裡會生成一個代理對象
                // 例如,我們可以在controller中直接注入request對象
                // 就是因為,容器啟動時就在resolvableDependencies放入了一個鍵值對
                // 其中key為:Request.class,value為:ObjectFactory
                // 在實際注入時放入的是一個代理對象
                autowiringValue = AutowireUtils.resolveAutowiringValue(autowiringValue, requiredType);
                if (requiredType.isInstance(autowiringValue)) {
                    // 這裏放入的key不是Bean的名稱
                    // value是實際依賴的對象
                    result.put(ObjectUtils.identityToString(autowiringValue), autowiringValue);
                    break;
                }
            }
        }
        
        // 接下來開始對之前查找出來的類型匹配的所有BeanName進行處理
        for (String candidate : candidateNames) {
            // 不是自引用,什麼是自引用?
            // 1.候選的Bean的名稱跟需要進行注入的Bean名稱相同,意味着,自己注入自己
            // 2.或者候選的Bean對應的factoryBean的名稱跟需要注入的Bean名稱相同,
            // 也就是說A依賴了B但是B的創建又需要依賴A
            // 要符合注入的條件
            if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, descriptor)) {
                // 調用addCandidateEntry,加入到返回集合中,後文有對這個方法的分析
                addCandidateEntry(result, candidate, descriptor, requiredType);
            }
        }
        
        // 排除自引用的情況下,沒有找到一個合適的依賴
        if (result.isEmpty() && !indicatesMultipleBeans(requiredType)) {
            // 1.先走fallback邏輯,Spring提供的一個擴展吧,感覺沒什麼卵用
            // 默認情況下fallback的依賴描述符就是自身
            DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
            for (String candidate : candidateNames) {
                if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor)) {
                    addCandidateEntry(result, candidate, descriptor, requiredType);
                }
            }
            // fallback還是失敗
            if (result.isEmpty()) {
                // 處理自引用
                // 從這裏可以看出,自引用的優先級是很低的,只有在容器中真正的只有這個Bean能作為
                // 候選者的時候,才會去處理,否則自引用是被排除掉的
                for (String candidate : candidateNames) {
                    if (isSelfReference(beanName, candidate) &&
                        // 不是一個集合或者
                        // 是一個集合,但是beanName跟candidate的factoryBeanName相同
                        (!(descriptor instanceof MultiElementDescriptor) || !beanName.equals(candidate)) &&
                        isAutowireCandidate(candidate, fallbackDescriptor)) {
                        addCandidateEntry(result, candidate, descriptor, requiredType);
                    }
                }
            }
        }
        return result;
    }
    
    
    // candidates:就是findAutowireCandidates方法要返回的候選集合
    // candidateName:當前的這個候選Bean的名稱
    // descriptor:依賴描述符
    // requiredType:依賴的類型
    private void addCandidateEntry(Map<String, Object> candidates, String candidateName,
                                   DependencyDescriptor descriptor, Class<?> requiredType) {
    	
        // 如果依賴是一個集合,或者容器中已經包含這個單例了
        // 那麼直接調用getBean方法創建或者獲取這個Bean
        if (descriptor instanceof MultiElementDescriptor || containsSingleton(candidateName)) {
            Object beanInstance = descriptor.resolveCandidate(candidateName, requiredType, this);
            candidates.put(candidateName, (beanInstance instanceof NullBean ? null : beanInstance));
        }
        // 如果依賴的類型不是一個集合,這個時候還不能確定到底要使用哪個依賴,
        // 所以不能將這些Bean創建出來,所以這個時候,放入candidates是Bean的名稱以及類型
        else {
            candidates.put(candidateName, getType(candidateName));
        }
    }
    

    處理屬性注入(@Autowired)

    postProcessProperties

    // 在applyMergedBeanDefinitionPostProcessors方法執行的時候,
    // 已經解析過了@Autowired註解(buildAutowiringMetadata方法)
    public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
        // 這裏獲取到的是解析過的緩存好的注入元數據
        InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
        try {
            // 直接調用inject方法
            // 存在兩種InjectionMetadata
            // 1.AutowiredFieldElement
            // 2.AutowiredMethodElement
            // 分別對應字段的屬性注入以及方法的屬性注入
            metadata.inject(bean, beanName, pvs);
        }
        catch (BeanCreationException ex) {
            throw ex;
        }
        catch (Throwable ex) {
            throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
        }
        return pvs;
    }
    
    字段的屬性注入
    // 最終反射調用filed.set方法
    protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
        Field field = (Field) this.member;
        Object value;
        if (this.cached) {
            // 第一次注入的時候肯定沒有緩存
            // 這裏也是對原型情況的處理
            value = resolvedCachedArgument(beanName, this.cachedFieldValue);
        } else {
            DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
            desc.setContainingClass(bean.getClass());
            Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
            Assert.state(beanFactory != null, "No BeanFactory available");
            TypeConverter typeConverter = beanFactory.getTypeConverter();
            try {
                // 這裏可以看到,對@Autowired註解在字段上的處理
                // 跟byType下自動注入的處理是一樣的,就是調用resolveDependency方法
                value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
            } catch (BeansException ex) {
                throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
            }
            synchronized (this) {
                // 沒有緩存過的話,這裏需要進行緩存
                if (!this.cached) {
                    if (value != null || this.required) {
                        this.cachedFieldValue = desc;
                        // 註冊Bean之間的依賴關係
                        registerDependentBeans(beanName, autowiredBeanNames);
                        // 如果這個類型的依賴只存在一個的話,我們就能確定這個Bean的名稱
                        // 那麼直接將這個名稱緩存到ShortcutDependencyDescriptor中
                        // 第二次進行注入的時候就可以直接調用getBean(beanName)得到這個依賴了
                        // 實際上正常也只有一個,多個就報錯了
                        // 另外這裡會過濾掉@Vlaue得到的依賴
                        if (autowiredBeanNames.size() == 1) {
                            String autowiredBeanName = autowiredBeanNames.iterator().next();
                            // 通過resolvableDependencies這個集合找的依賴不滿足containsBean條件
                            // 不會進行緩存,因為緩存實際還是要調用getBean,而resolvableDependencies
                            // 是沒法通過getBean獲取的
                            if (beanFactory.containsBean(autowiredBeanName) &&
                                beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {							 // 依賴描述符封裝成ShortcutDependencyDescriptor進行緩存
                                this.cachedFieldValue = new ShortcutDependencyDescriptor(
                                    desc, autowiredBeanName, field.getType());
                            }
                        }
                    } else {
                        this.cachedFieldValue = null;
                    }
                    this.cached = true;
                }
            }
        }
        if (value != null) {
            // 反射調用Field.set方法
            ReflectionUtils.makeAccessible(field);
            field.set(bean, value);
        }
    }
    
    方法的屬性注入
    // 代碼看着很長,實際上邏輯跟字段注入基本一樣
    protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
        // 判斷XML中是否配置了這個屬性,如果配置了直接跳過
        // 換而言之,XML配置的屬性優先級高於@Autowired註解
        if (checkPropertySkipping(pvs)) {
            return;
        }
        Method method = (Method) this.member;
        Object[] arguments;
        if (this.cached) {
            arguments = resolveCachedArguments(beanName);
        } else {
            // 通過方法參數類型構造依賴描述符
            // 邏輯基本一樣的,最終也是調用beanFactory.resolveDependency方法
            Class<?>[] paramTypes = method.getParameterTypes();
            arguments = new Object[paramTypes.length];
            DependencyDescriptor[] descriptors = new DependencyDescriptor[paramTypes.length];
            Set<String> autowiredBeans = new LinkedHashSet<>(paramTypes.length);
            Assert.state(beanFactory != null, "No BeanFactory available");
            TypeConverter typeConverter = beanFactory.getTypeConverter();
            
            // 遍歷方法的每個參數
            for (int i = 0; i < arguments.length; i++) {
                MethodParameter methodParam = new MethodParameter(method, i);
                DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required);
                currDesc.setContainingClass(bean.getClass());
                descriptors[i] = currDesc;
                try {
                    // 還是要調用這個方法
                    Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter);
                    if (arg == null && !this.required) {
                        arguments = null;
                        break;
                    }
                    arguments[i] = arg;
                } catch (BeansException ex) {
                    throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex);
                }
            }
            synchronized (this) {
                if (!this.cached) {
                    if (arguments != null) {
                        Object[] cachedMethodArguments = new Object[paramTypes.length];
                        System.arraycopy(descriptors, 0, cachedMethodArguments, 0, arguments.length);  
                        // 註冊bean之間的依賴關係
                        registerDependentBeans(beanName, autowiredBeans);
                        
                        // 跟字段注入差不多,存在@Value註解,不進行緩存
                        if (autowiredBeans.size() == paramTypes.length) {
                            Iterator<String> it = autowiredBeans.iterator();
                            for (int i = 0; i < paramTypes.length; i++) {
                                String autowiredBeanName = it.next();
                                if (beanFactory.containsBean(autowiredBeanName) &&
                                    beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) {
                                    cachedMethodArguments[i] = new ShortcutDependencyDescriptor(
                                        descriptors[i], autowiredBeanName, paramTypes[i]);
                                }
                            }
                        }
                        this.cachedMethodArguments = cachedMethodArguments;
                    } else {
                        this.cachedMethodArguments = null;
                    }
                    this.cached = true;
                }
            }
        }
        if (arguments != null) {
            try {
                // 反射調用方法
                // 像我們的setter方法就是在這裏調用的
                ReflectionUtils.makeAccessible(method);
                method.invoke(bean, arguments);
            } catch (InvocationTargetException ex) {
                throw ex.getTargetException();
            }
        }
    }
    

    處理依賴檢查

    protected void checkDependencies(
        String beanName, AbstractBeanDefinition mbd, PropertyDescriptor[] pds, PropertyValues pvs)
        throws UnsatisfiedDependencyException {
    
        int dependencyCheck = mbd.getDependencyCheck();
        for (PropertyDescriptor pd : pds) {
            
            // 有set方法但是在pvs中沒有對應屬性,那麼需要判斷這個屬性是否要進行依賴檢查
            // 如果需要進行依賴檢查的話,就需要報錯了
            // pvs中保存的是自動注入以及XML配置的屬性
            if (pd.getWriteMethod() != null && !pvs.contains(pd.getName())) {
               
                // 是否是基本屬性,枚舉/日期等也包括在內
                boolean isSimple = BeanUtils.isSimpleProperty(pd.getPropertyType());
               	
                // 如果DEPENDENCY_CHECK_ALL,對任意屬性都開啟了依賴檢查,報錯
                // DEPENDENCY_CHECK_SIMPLE,對基本屬性開啟了依賴檢查並且是基本屬性,報錯
                // DEPENDENCY_CHECK_OBJECTS,對非基本屬性開啟了依賴檢查並且不是非基本屬性,報錯
                boolean unsatisfied = (dependencyCheck == AbstractBeanDefinition.DEPENDENCY_CHECK_ALL) ||
                    (isSimple && dependencyCheck == AbstractBeanDefinition.DEPENDENCY_CHECK_SIMPLE) ||
                    (!isSimple && dependencyCheck == AbstractBeanDefinition.DEPENDENCY_CHECK_OBJECTS);
                
                if (unsatisfied) {
                    throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, pd.getName(),
                                                             "Set this property value or disable dependency checking for this bean.");
                }
            }
        }
    }
    

    將解析出來的屬性應用到Bean上

    到這一步解析出來的屬性主要有三個來源

    1. XML中配置的
    2. 通過byName的方式自動注入的
    3. 通過byType的方式自動注入的

    但是在應用到Bean前還需要做一步類型轉換,這一部分代碼實際上跟我們之前在Spring官網閱讀(十四)Spring中的BeanWrapper及類型轉換介紹的差不多,而且因為XML跟自動注入的方式都不常見,正常@Autowired的方式進行注入的話,這個方法沒有什麼用,所以本文就不再贅述。

    總結

    本文我們主要分析了Spring在屬性注入過程中的相關代碼,整個屬性注入可以分為兩個部分

    1. @Autowired/@Vale的方式完成屬性注入
    2. 自動注入(byType/byName

    完成屬性注入的核心方法其實就是doResolveDependencydoResolveDependency這個方法的邏輯簡單來說分為兩步:

    1. 通過依賴類型查詢到所有的類型匹配的bean的名稱
    2. 如果找到了多個的話,再根據依賴的名稱匹配對應的Bean的名稱
    3. 調用getBean得到這個需要被注入的Bean
    4. 最後反射調用字段的set方法完成屬性注入

    從上面也可以知道,其實整個屬性注入的邏輯是很簡單的。

    如果本文對你有幫助的話,記得點個贊吧!也歡迎關注我的公眾號,微信搜索:程序員DMZ,或者掃描下方二維碼,跟着我一起認認真真學Java,踏踏實實做一個coder。

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

    【其他文章推薦】

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

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

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

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

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

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

  • 為什麼用抓包工具看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網頁設計為架站首選

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

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

    ※回頭車貨運收費標準

  • 三文搞懂學會Docker容器技術(上)

    三文搞懂學會Docker容器技術(上)

    1,Docker簡介

      1.1 Docker是什麼?

    Docker官網: https://www.docker.com/

    Docker 是一個開源的應用容器引擎,基於 Go 語言 並遵從Apache2.0協議開源。
    Docker 可以讓開發者打包他們的應用以及依賴包到一個輕量級、可移植的容器中,然後發布到任何流行的 Linux 機器上,也可以實現虛擬化。
    容器是完全使用沙箱機制,相互之間不會有任何接口(類似 iPhone 的 app),更重要的是容器性能開銷極低。
    Docker 從 17.03 版本之後分為 CE(Community Edition: 社區版) 和 EE(Enterprise Edition: 企業版),我們用社區版就可以了。

      1.2 Docker架構原理?

     

    Docker三要素,鏡像,容器,倉庫

    1.鏡像

    Docker 鏡像(Image)就是一個只讀的模板,它可以是一個可運行軟件(tomcat,mysql),也可以是一個系統(centos)。鏡像可以用來創建 Docker 容器,一個鏡像可以創建很多容器。

    2.容器

    Docker 利用容器(Container)獨立運行的一個或一組應用。容器是用鏡像創建的運行實例。它可以被啟動、開始、停止、刪除。每個容器都是相互隔離的、保證安全的平台。可以把容器看做是一個簡易版的 Linux 環境(包括root用戶權限、進程空間、用戶空間和網絡空間等)和運行在其中的應用程序。容器的定義和鏡像幾乎一模一樣,也是一堆層的統一視角,唯一區別在於容器的最上面那一層是可讀可寫的。

    3.倉庫

    倉庫(Repository)是集中存放鏡像文件的場所,類似GitHub存放項目代碼一樣,只不過Docker Hub是由來存鏡像(image)的。倉庫(Repository)和倉庫註冊服務器(Registry)是有區別的。倉庫註冊服務器上往往存放着多個倉庫,每個倉庫中又包含了多個鏡像,每個鏡像有不同的標籤(tag,類似版本號)。

    倉庫分為公開倉庫(Public)和私有倉庫(Private)兩種形式。

    最大的公開倉庫是 Docker Hub(https://hub.docker.com/),存放了數量龐大的鏡像供用戶下載。國內的公開倉庫包括阿里雲 、網易雲 等。

     

    容器與鏡像的關係類似於面向對象編程中的對象與類。

    Docker 面向對象
    容器 對象
    鏡像

      1.3 Docker有什麼用?

        1,簡化環境搭建,提高開發生命周期效率;

        2,大大簡化運維工作量;

        3,微服務利器;

      1.4 Docker容器與虛擬機區別?

    Docker是一種輕量級的虛擬化技術,比傳統的虛擬機性能更好。

    下圖是虛擬機的體繫結構:

     

    • server – 表示真實電腦。
    • Host OS – 真實電腦的操作系統,例如:Windows,Linux
    • Hypervisor – 虛擬機平台,模擬硬件,如VMWare,VirtualBox
    • Guest OS – 虛擬機平台上安裝的操作系統,例如CentOS Linux
    • App – 虛擬機操作系統上的應用,例如nginx

     

    下圖是Docker的體繫結構:

    • server – 表示真實電腦。
    • Host OS – 真實電腦的操作系統,例如:Windows,Linux
    • Docker Engine – 新一代虛擬化技術,不需要包含單獨的操作系統。
    • App – 所有的應用程序現在都作為Docker容器運行。

     

    這種體繫結構的明顯優勢是,不需要為虛擬機操作系統提供硬件模擬。所有應用程序都作為Docker容器工作,性能更好。

      Docker容器 虛擬機(VM)
    操作系統 與宿主機共享OS 宿主機OS上運行宿主機OS
    存儲大小 鏡像小,便於存儲與傳輸 鏡像龐大(vmdk等)
    運行性能 幾乎無額外性能損失 操作系統額外的cpu、內存消耗
    移植性 輕便、靈活、適用於Linux 笨重、與虛擬化技術耦合度高
    硬件親和性  面向軟件開發者 面向硬件運維者

     

    Docker優點:輕量級,速度快,運行應用隔離,方便維護…

    2,Docker安裝

      2.1 Docker版本介紹

    Docker從1.13版本之後採用時間線的方式作為版本號,分為社區版CE和企業版EE。

    社區版是免費提供給個人開發者和小型團體使用的,企業版會提供額外的收費服務,比如經過官方測試認證過的基礎設施、容器、插件等。

    社區版按照stable和edge兩種方式發布,每個季度更新stable版本,如17.06,17.09;每個月份更新edge版本,如17.09,17.10。

    我們平時用社區版就足夠了。所以我們安裝社區版;

      2.2 Docker安裝官方文檔

    我們主要參考:https://docs.docker.com/install/linux/docker-ce/centos/  來安裝;

      2.3 工具準備

    前置課程:Centos課程  http://www.java1234.com/javaxuexiluxiantu.html

    打包下載: http://pan.baidu.com/s/1i55jJAt

    虛擬機 VMware

    centos7安裝下虛擬機VM上;

    連接工具 才用 FinalShell  官方地址:http://www.hostbuf.com/

      2.4 Docker安裝步驟

    我們切換到root用戶

    1、Docker 要求 CentOS 系統的內核版本高於 3.10 ,查看本頁面的前提條件來驗證你的CentOS 版本是否支持 Docker 。

    通過 uname -r 命令查看你當前的內核版本

     $ uname -r

    2、使用 root 權限登錄 Centos。確保 yum 包更新到最新。

    $ yum update

    3、卸載舊版本(如果安裝過舊版本的話)

    $ yum remove docker  docker-common docker-selinux docker-engine

    4、安裝需要的軟件包, yum-util 提供yum-config-manager功能,另外兩個是devicemapper驅動依賴的

    $ yum install -y yum-utils device-mapper-persistent-data lvm2

    5、設置yum源

    $ yum-config-manager –add-repo https://download.docker.com/linux/centos/docker-ce.repo

    6,安裝最新版本的Docker

    $ yum install docker-ce docker-ce-cli containerd.io

    7,啟動Docker並設置開機啟動

    $ systemctl start docker

    $ systemctl enable docker

    8,驗證Docker

    $ docker version

     

    說明安裝OK;

    9,Docker HelloWorld測試;

    $ docker run hello-world

     

    因為本地沒有這個鏡像,所以從遠程官方倉庫去拉取,下載;

    然後我們再執行一次;

     

    OK了

      2.5 Docker配置阿里雲鏡像倉庫

    Docker默認遠程倉庫是 https://hub.docker.com/

    比如我們下載一個大點的東西,龜速

     

    由於是國外主機,類似Maven倉庫,慢得一腿,經常延遲,破損;

    所以我們一般都是配置國內鏡像,比如阿里雲,網易雲等;推薦阿里雲,穩定點;

    配置步驟如下:

    1,登錄進入阿里雲鏡像服務中心,獲取鏡像地址

    進入阿里雲容器鏡像服務地址:點這裏快速進入

    使用你的淘寶賬號密碼登錄

     

    這裏我們獲取鏡像地址;

    2,在/etc/docker目錄下找到在daemon.json文件(沒有就新建),將下面內容寫入

    {

     “registry-mirrors”: [“https://xxxxxxx.mirror.aliyuncs.com”]

    }

    3,重啟daemon

    systemctl daemon-reload

    4,重啟docker服務

    systemctl restart docker

    5,測試

    由於速度太快,截圖都難;

     

    3,HelloWorld運行原理

    運行  docker run hello-world

    本地倉庫未能找到該鏡像,然後去遠程倉庫尋找以及下載該鏡像;

    然後我們再執行該命令:

    出來了 Hellowold。我們具體來分析下 執行原理和過程;

    從左到右 client客戶端,Docker運行主機,遠程倉庫;

    docker build ,pull,run分別是 構建,拉取,運行命令,後面再細講;

    中間Docker主機里有 Docker daemon主運行線程,以及Containers容器,容器里可以運行很多實例,(實例是從右側Images鏡像實例化出來的)Images是存儲再本地的鏡像文件,比如 Redis,Tomat這些鏡像文件;

    右側是Registry鏡像倉庫,默認遠程鏡像倉庫 https://hub.docker.com/  不過是國外主機,下載很慢,不穩定,所以我們後面要配置成阿里雲倉庫鏡像地址,穩定快捷;

    執行 docker run hello-world的過程看如下圖例:

     

     

     

    4,Docker基本命令

       4.1 啟動Docker

               systemctl start docker

      4.2 停止Docker

             systemctl stop docker

      4.3 重啟Docker

           systemctl restart docker

      4.4 開機啟動Docker

         systemctl enable docker

      4.5 查看Docker概要信息

       docker info

      4.6 查看Docker幫助文檔

       docker –help

      4.7 查看Docker版本信息

         docker version

    5,Docker鏡像

      5.1 docker images 列出本機所有鏡像

     

    REPOSITORY 鏡像的倉庫源
    TAG 鏡像的標籤(版本)同一個倉庫有多個TAG的鏡像,多個版本;我們用REPOSITORY:TAG來定義不同的鏡像;
    IMAGE ID 鏡像ID,鏡像的唯一標識
    CREATE 鏡像創建時間
    SIZE 鏡像大小

    OPTIONS 可選參數:

    -a 显示所有鏡像(包括中間層)
    q 只显示鏡像ID
    -qa 可以組合
    –digests 显示鏡像的摘要信息
    –no-trunc 显示完整的鏡像信息 

     

      5.2 docker search 搜索鏡像

    和 https://hub.docker.com/ 這裏的搜索效果一樣;

    OPTIONS可選參數:

    –no-trunc 显示完整的鏡像描述
    -s 列出收藏數不小於指定值的鏡像
    –automated 只列出Docker Hub自動構建類型的鏡像

     

     

     

      5.3 docker pull 下載鏡像

    docker pull 鏡像名稱:[TAG]

    注意:不加TAG,默認下載最新版本latest

      5.4 docker rmi 刪除鏡像

    1,刪除單個:docker rmi 鏡像名稱:[TAG]

    如果不寫TAG,默認刪除最新版本latest

    有鏡像生成的容器再運行時候,會報錯,刪除失敗;

    我們需要加 -f 強制刪除

    2,刪除多個:docker rmi -f 鏡像名稱1:[TAG] 鏡像名稱2:[TAG]

    中間空格隔開

    3,刪除全部:docker rmi -f $(docker images -qa)

     

     

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

    作者: java1234_小鋒

    出處:https://www.cnblogs.com/java688/p/13132444.html

    版權:本站使用「CC BY 4.0」創作共享協議,轉載請在文章明顯位置註明作者及出處。

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

     

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

    【其他文章推薦】

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

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

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

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

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

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

  • 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維修中心