電動巴士車廠華德今年拚損益兩平 年底前登錄興櫃

電動巴士車廠華德動能於 11 月 3 日與群益金鼎證券簽訂股票上市櫃輔導契約,目標今年底前登錄興櫃,華德動能董事長蔡易忠表示,今年電動巴士預計出貨 53 輛 ,市占全台第一大,今年力拚損益兩平。   華德為台灣第二家電動巴士掛牌企業,同時也是車王電子轉投資公司,車王電持有比例為 14.9%,華德動能去年合併營收 5,782 萬元,虧損 6,000 萬元,每股虧損 4.73 元。今年上半年營收躍升至 2.28 億元,並順利轉盈,稅後純益 1,300 萬元,每股稅後純益 0.65 元。   蔡易忠表示,公司主要業務為電動巴士、電動商用貨車以及儲能系統三大主軸。華德電動巴士使用的電池為稀土鋰釔電池,不同於 F-立凱電為採用磷酸鐵鋰材料,蔡易忠表示,只要電池材料能讓電動巴士行駛安全、性能佳,都會持續找尋新的電池材料,不會局限在單一材料。   蔡易忠表示,華德積極布局開發海外市場,未來擬將底盤系統或關鍵動力組件出口至東南亞、中國、美國及歐洲地區,近期先出口至馬來西亞。目前公司也積極規劃建廠計畫,擬以租賃方式承租廠房,計劃由年產 80 輛擴大至 500 輛,目標 2015 年第 3 季加入營運。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

宣揚減塑環保 印尼偶戲師回收寶特瓶製玩偶

摘錄自2019年10月29日民視新聞報導

印尼龍目島有一位玩偶大師,為了讓小朋友了解塑膠污染的可怕,因而回收塑膠寶特瓶再廢物利用,製做成演出的玩偶。

劇團的玩偶,從頭到腳,全是塑膠製品。團長拉提夫擔心印尼偶戲傳統失傳,2015年創辦了偶劇團。後來因感到塑膠製品危害無窮,2018年起開始回收塑膠垃圾,主要是寶特瓶,並廢物利用做成玩偶,希望讓小朋友學習從家裡開始管理垃圾,以及養成不亂丟垃圾的習慣。

劇團成員的年齡在7歲到16歲之間。大家先在自家附近撿拾寶特瓶,之後再分類、清洗。接著就發揮創意,將這些瓶子上色、製做耳鼻、手腳等。觀眾不分大人、小孩,都很認同劇團的用心。

劇團原本只在龍目島演出。由於名聲漸漸打開來,開始進軍首都雅加達,也希望能將環保愛地球的理念帶出去。

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

【其他文章推薦】

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

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

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

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

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

2014中國高新汽車國際峰會圓滿閉幕,廣受業界好評

第三屆中國高新汽車國際峰會已於2014年12月9日在上海浦東嘉里大酒店完美落幕。本屆峰會由法蘭克福展覽(上海)有限公司和中國國家發展和改革委員會國際合作中心聯合主辦,共邀請到包括政府官員、行業協會代表和知名整車企業在內的14位重量級演講嘉賓出席,共同探討未來汽車產業革新的重要創新技術、市場趨勢和發展戰略方針。為期一整天的峰會由一個主論壇和兩個分論壇組成,共迎來386位與會代表,專業領域橫跨汽車全產業鏈。

法蘭克福展覽(上海)有限公司總經理李慶新先生表示:“由於整體定位和結構調整,峰會由往年的兩天精簡為一天時間,但是本屆峰會的參會代表人數仍再創新高。由此印證了中國高新汽車國際峰會作為汽車行業的年度盛會,為業內重要政策決策者、企業高管和投資者建立了交流契機和合作機會。與會代表對我們表達了支持和鼓勵,我們非常高興可以收到如此正面的肯定。我們相信中國高新汽車國際峰會將會成為討論汽車行業發展趨勢、最新技術以及業界精英交流討論的高端平台。”

參會代表和演講嘉賓滿意會議定位,共同關注汽車行業未來合作契機

本屆峰會以「關注未來汽車發展趨勢,聚焦零部件產業升級」為主題,共邀請到多位政府領導蒞臨並致辭。其中包括:

  • 鄭惠強先生,全國政協常委、上海市人大副主任
  • 李鋼先生,國家發改委產業協調司處長
  • 徐秉金先生,中歐協會會長、商務部原副部長

峰會由上海市新能源汽車推進辦公室主任劉建華先生擔任首位演講嘉賓,就「發起『新能源汽車走進社區』倡議」議題發表真知灼見。除此之外,強大演講嘉賓陣容包括來自寶馬、特斯拉、沃爾沃、上汽和北汽等國內外知名整車企業。首次擔任演講嘉賓的上海汽車集團股份有限公司汽車工程中心車身開發總監邱國華博士此次主要圍繞上汽集團電動汽車發展戰略展開介紹,並評價道:“中國高新汽車國際峰會是一個高端的汽車行業交流平台,議題定位準確。主題覆蓋整車、能源和材料等一系熱門話題。峰會聚集了汽車行業的技術戰略研究和市場分析等高端人才,我認為這是一個非常出色的活動。”

特斯拉中國區公共政策和充電基礎設施總監高翔先生同樣也分享了他的感想,並表示:“本屆峰會邀請了很多政府政策制定者,他們的政策導向對於未來電動車的發展影響很大。我認為電動汽車產業正得到大眾越來越多的關注。我希望通過中國高新汽車國際峰會這個深入的交流平台,在政策環境和行業環境得到突破,未來能夠制定完善的行業標準,希望有更多的車企投入到電動汽車的開發中。”

上海大眾汽車有限公司信息系統部高級經理邱振捷先生擔任”用互聯網思維造車”主題的演講嘉賓,關注電動汽車行業的前瞻性概念。他認為:“去年我是峰會的參會代表,而今年我非常榮幸可以擔任演講嘉賓出席本屆峰會。中國高新汽車國際峰會是一個齊聚行業精英的平台,活動的整體規劃和安排超出我的預期。在這裏我遇到了很多零部件企業和整車廠,我非常願意和他們互相交流經驗,這給我工作帶來很多幫助。”

已經連續第二年出席的三菱綜合材料管理(上海)有限公司戰略企劃部經理徐冬榮先生對峰會在新能源汽車未來發展前景產生積極作用表示肯定,說道:“演講嘉賓介紹了最新的電動汽車技術和資訊,通過本屆峰會我瞭解到很多高新汽車的最新行業發展趨勢和扶持政策。我認為新能源汽車的未來發展前景是非常巨大的,因此發展速度和技術瓶頸將是最需要關注的問題。”

首次參會的參會代表德國ThyssenKrupp System Engineering GmbH的業務發展專員Elena Kaplun女士希望通過中國高新汽車國際峰會洞察中國汽車行業的最新潛在投資機會,她表示道:“我對中國汽車市場有了深入的認識和瞭解。我對新能源汽車和輕量化汽車技術發展的話題很感興趣,嘉賓的演講內容對市場研究和業務發展戰略規劃很有幫助。除此之外,我還有機會在這裏拓展我的人脈,更可以和卓越的業界人士有進一步的合作機會如特斯拉、BMW和上汽等,我非常高興能夠參加此次活動!”

2014 年中國高新汽車國際峰會同期舉行的 Automechanika Shanghai — 上海國際汽車零配件、維修檢測診斷設備及服務用品展覽會,是亞洲規模最大的汽車零部件、維修檢測診斷設備及汽車用品展覽會。

欲瞭解更多有關峰會詳情,敬請訪問官方網站

或發郵件至。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

4. union-find算法

  算法的主題思想:

  1.優秀的算法因為能夠解決實際問題而變得更為重要;

  2.高效算法的代碼也可以很簡單;

  3.理解某個實現的性能特點是一個挑戰;

  4.在解決同一個問題的多種算法之間進行選擇時,科學方法是一種重要的工具;

  5.迭代式改進能夠讓算法的效率越來越高效;

 

   1. 動態連通性

  動態連接:輸入是一對整數對的序列,其中每個整數代表某種類型的對象(或觸點),我們將整數對p q 解釋為意味着p連接到q。我們假設“連接到”是等價關係:

  對稱性:如果p連接到q,則q 連接到p。

  傳遞性:如果p連接到q且q 連接到r,則p連接到r。
  自反性:p與p連接。
  等價關係將對象劃分為多個等價類 或連接的組件。等價類稱為連通分量或分量。
  我們的目標是編寫一個程序,以從序列中過濾掉多餘的對:當程序從輸入中讀取整數對 p q時,只有在該對點不等價的情況下,才應將對寫入到輸出中,並且將p連接到q。如果等價,則程序應忽略整數對pq 並繼續讀取下對。

  

 

  動態連通性問題的應用:

    1.網絡

    2.變量名等價性

    3.數學集合

      在更高的抽象層次上,可以將輸入的所有整數看做屬於不同的數學集合。

   2. 定義問題

  設計算法的第一個任務就是精確地定義問題。

  算法解決的問題越大,它完成任務所需的時間和空間可能越多。我們不可能預先知道這其間的量化關係,通常只會在發現解決問題很困難,或是代價巨大,或是發現算法所提供的信息比原問題所需要的更加有用時修改問題。例如,連通性問題只要求我們的程序能夠判斷出給定的整數對是否相連,但並沒有要求給出兩者之間的通路上的所有連接。這樣的要求更難,並會得出另一組不同的算法。

  為了定義和說明問題,先設計一份API  來封裝基本操作: 初始化,連接兩個觸點,查找某個觸點的分量 ,判斷兩個觸點是否屬於同一分量,分量的數量:

    /// <summary>
    /// 動態連通API
    /// </summary>
    public interface IUnionFind
    {
        /// <summary>
        /// 連接
        /// </summary>
        /// <param name="p"></param>
        /// <param name="q"></param>
        void Union(int p, int q);

        /// <summary>
        /// 查找觸點 p 的分量標識符
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        int Find(int p);

        /// <summary>
        /// 判斷兩個觸點是否處於同一分量
        /// </summary>
        /// <param name="p"></param>
        /// <param name="q"></param>
        /// <returns></returns>
        bool Connected(int p, int q);

        /// <summary>
        /// 連通分量的數量
        /// </summary>
        /// <returns></returns>
        int Count();
    }

  為解決動態連通性問題設計算法的任務轉化為實現這份API:

    1. 定義一種數據結構表示已知的連接;

    2. 基於此數據結構高效的實現API的方法;

  數據結構的性質會直接影響算法的效率。這裏,觸點為索引,觸點和連接分量都是用 int 值表示,將會使用分量中某個觸點的值作為分量的標識符。所以,一開始,每個觸點都是只含有自己的分量,分量標識符為觸點的值。由此,可以初步實現一部分方法:

 

    public class FirstUnionFind:IUnionFind
    {
        private int[] id;//* 分量id 以觸點作為索引
        private int count;//分量數量

        public FirstUnionFind(int n)
        {
            count = n;
            id = new int[n];
            for (var i = 0; i < n; i++)
            {
                id[i] = i; // 第一個 i 作為觸點,第二個 i 作為觸點的值
            }
        }

        public int Count()
        {
            return count;
        }

        public bool Connected(int p, int q)
        {
            return Find(p) == Find(q);
        }

        public int Find(int p)
        {
            
        }

        public void Union(int p, int q)
        {
            
        }
    }

 

  Union-find 的成本模型 是數組的訪問次數(無論讀寫)。

   3. quick-find算法實現

  quick-find 算法是保證當且僅當 id[p] 等於 id[q] 時,p 和 q 是連通的。也就是說,在同一個連通分量中的所有觸點在 id[ ] 中的值全部相等。

  所以 Find 方法只需返回 id[q],Union 方法需要先判斷 Find(p)  是否等於 Find(q) ,若相等直接返回;若不相等,需要將 q 所在的連通分量中所有觸點的 id [ ] 值全部更新為 id[p]。

    public class QuickFindUF: IUnionFind
    {
        private int[] id;//* 分量id 以觸點作為索引
        private int count;//分量數量

        public QuickFindUF(int n)
        {
            count = n;
            id = new int[n];
            for (var i = 0; i < n; i++)
            {
                id[i] = i; // 第一個 i 作為觸點,第二個 i 作為觸點的值
            }
        }

        public int Count()
        {
            return count;
        }

        public bool Connected(int p, int q)
        {
            return Find(p) == Find(q);
        }

        public int Find(int p)
        {
            return id[p];
        }

        public void Union(int p, int q)
        {
            var pID = Find(p);
            var qID = Find(q);

            if (pID == qID)
                return;

            for (var i = 0; i < id.Length; i++)
            {
                if (id[i] == qID)
                    id[i] = pID;
            }
            count--; //連通分量減少
        }

        public void Show()
        {
            for(var i = 0;i<id.Length;i++)
                Console.WriteLine("索引:"+i+",值:"+ id[i] );
            Console.WriteLine("連通分量數量:"+count);
        }
    }

  

  算法分析

  Find() 方法只需訪問一次數組,所以速度很快。但是對於處理大型問題,每對輸入 Union() 方法都需要掃描整個數組。

  每一次歸併兩個分量的 Union() 方法訪問數組的次數在 N+3 到 2N+1 之間。由代碼可知,兩次 Find 操作訪問兩次數組,掃描數組會訪問N次,改變其中一個分量中所有觸點的值需要訪問 1 到 N – 1 次(最好情況是該分量中只有一個觸點,最壞情況是該分量中有 N – 1個觸點),2+N+N-1。

  如果使用quick-find 算法來解決動態連通性問題並且最後只得到一個連通分量,至少需要調用 N-1 次Union() 方法,那麼至少需要 (N+3)(N-1) ~ N^2 次訪問數組,是平方級別的。

 

   4. quick-union算法實現

  quick-union 算法重點提高 union 方法的速度,它也是基於相同的數據結構 — 已觸點為索引的 id[ ] 數組,但是 id[ ] 的值是同一分量中另一觸點的索引(名稱),也可能是自己(根觸點)——這種聯繫成為鏈接。

  在實現 Find() 方法時,從給定觸點,鏈接到另一個觸點,知道到達根觸點,即鏈接指向自己。同時修改 Union() 方法,分別找到 p q 的根觸點,將其中一個根觸點鏈接到根觸點。

public class QuickUnionUF : IUnionFind
    {
        private int[] id;
        private int count;

        public QuickUnionUF(int n)
        {
            count = n;
            id = new int[n];
            for (var i = 0; i < n; i++)
            {
                id[i] = i; // 第一個 i 作為觸點,第二個 i 作為觸點的值
            }
        }

        public int Count()
        {
            return count;
        }

        public bool Connected(int p, int q)
        {
            return Find(p) == Find(q);
        }

        public int Find(int p)
        {
            while (p != id[p])
                p = id[p];
            return p;
        }

        public void Union(int p, int q)
        {
            var pRoot = Find(p);
            var qRoot = Find(q);

            if (pRoot == qRoot)
                return;

            id[pRoot] =qRoot;
count
--; //連通分量減少 } public void Show() { for (var i = 0; i < id.Length; i++) Console.WriteLine("索引:" + i + ",值:" + id[i]); Console.WriteLine("連通分量數量:" + count); } }

  

  森林表示

  id[ ] 數組用父鏈接的形式表示一片森林,用節點表示觸點。無論從任何觸點所對應的節點隨着鏈接查找,最後都將到達含有該節點的根節點。初始化數組之後,每個節點的鏈接都指向自己。

 

  算法分析

  定義:一棵樹的大小是它的節點的數量。樹中一個節點的深度是它到根節點的路徑上鏈接數。樹的高度是它的所有節點中的最大深度。

  quick-union 算法比 quick-find 算法更快,因為它對每對輸入不需要遍歷整個數組。

  分析quick-union 算法的成本比 quick-find 算法的成本要困難,因為quick-union 算法依賴於輸入的特點。在最好的情況下,find() 方法只需訪問一次數組就可以得到一個觸點的分量表示;在最壞情況下,需要 2i+1 次數組訪問(i 時觸點的深度)。由此得出,該算法解決動態連通性問題,在最佳情況下的運行時間是線性級別,最壞情況下的輸入是平方級別。解決了 quick-find 算法中 union() 方法總是線性級別,解決動態連通性問題總是平方級別。

  quick-union 算法中 find() 方法訪問數組的次數為 1(到達根節點只需訪問一次) 加上 給定觸點所對應節點的深度的兩倍(while 循環,一次讀,一次寫)。union() 訪問兩次 find() ,如果兩個觸點不在同一分量還需加一次寫數組。

   假設輸入的整數對是有序的 0-1, 0-2,0-3 等,N-1 對之後N個觸點將全部處於相同的集合之中,且得到的樹的高度為 N-1。由上可知,對於整數對 0-i , find() 訪問數組的次數為 2i + 1,因此,處理 N 對整數對所需的所有訪問數組的總次數為 3+5+7+ ……+(2N+1) ~ n^2

  

  

  5.加權 quick-union 算法實現

  簡單改動就可以避免 quick-union算法 出現最壞情況。quick-union算法 union 方法是隨意將一棵樹連接到另一棵樹,改為總是將小樹連接到大樹,這需要記錄每一棵樹的大小,稱為加權quick-union算法。

  代碼:

    public class WeightedQuickUnionUF: IUnionFind
    {
        int[] sz;//以觸點為索引的 各個根節點對應的分量樹大小
        private int[] id;
        private int count;

        public WeightedQuickUnionUF(int n)
        {
            count = n;
            id = new int[n];
            sz = new int[n];
            for (var i = 0; i < n; i++)
            {
                id[i] = i; // 第一個 i 作為觸點,第二個 i 作為觸點的值
                sz[i] = 1;
            }
        }

        public int Count()
        {
            return count;
        }

        public bool Connected(int p, int q)
        {
            return Find(p) == Find(q);
        }

        public int Find(int p)
        {
            while (p != id[p])
                p = id[p];
            return p;
        }

        public void Union(int p, int q)
        {
            var pRoot = Find(p);
            var qRoot = Find(q);

            if (pRoot == qRoot)
                return;

            if (sz[pRoot] < sz[qRoot])
            {
                id[pRoot] = qRoot;
            }
            else
            {
                id[qRoot] = pRoot;
            }
                
            count--; //連通分量減少
        }

        public void Show()
        {
            for (var i = 0; i < id.Length; i++)
                Console.WriteLine("索引:" + i + ",值:" + id[i]);
            Console.WriteLine("連通分量數量:" + count);
        }
    }

 

  算法分析

  加權 quicj-union 算法最壞的情況:

  

  這種情況,將要被歸併的樹的大小總是相等的(且總是 2 的 冥),都含有 2^n 個節點,高度都正好是 n 。當歸併兩個含有 2^n 個節點的樹時,得到的樹含有 2 ^ n+1 個節點,高度增加到 n+1 。

  節點大小: 1  2  4  8  2^k = N

  高       度: 0  1  2  3  k

  k = logN

  所以加權 quick-union 算法可以保證對數級別的性能。

  對於 N 個觸點,加權 quick-union 算法構造的森林中的任意節點的深度最多為logN。

  對於加權 quick-union 算法 和 N 個觸點,在最壞情況下 find,connected 和 union 方法的成本的增長量級為 logN。

  對於動態連通性問題,加權 quick-union 算法 是三種算法中唯一可以用於解決大型問題的算法。加權 quick-union 算法 處理 N 個觸點和 M 條連接時最多訪問數組 c M logN 次,其中 c 為常數。

  

  三個算法處理一百萬個觸點運行時間對比:

  

  

  三個算法性能特點:

 

  6.最優算法 – 路徑壓縮

  在檢查節點的同時將它們直接連接到根節點。

  實現:為 find 方法添加一個循環,將在路徑上的所有節點都直接鏈接到根節點。完全扁平化的樹。

 

  

  研究各種基礎問題的基本步驟:

  1. 完整而詳細地定義問題,找出解決問題所必須的基本抽象操作並定義一份API。

  2. 簡潔地實現一種初級算法,給出一個精心組織的開發用例並使用實際數據作為輸入。

  3. 當實現所能解決的問題的最大規模達不到期望時決定改進還是放棄。

  4. 逐步改進實現,通過經驗性分析和數學分析驗證改進后的效果。

  5. 用更高層次的抽象表示數據結構或算法來設計更高級的改進版本。

  6. 如果可能盡量為最壞情況下的性能提供保證,但在處理普通數據時也要有良好的性能。

  7.在適當的時候將更細緻的深入研究留給有經驗的研究者並解決下一個問題。

 

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

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

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

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

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

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

※回頭車貨運收費標準

深圳今年投254億推新能源車 純電動客車補貼高達254萬

日前,深圳市政府推出了《深圳市新能源發展工作方案》和《深圳市新能源汽車推廣應用若干政策措施》,成為2015年以來中國第一個推出新能源汽車扶持政策的城市。深圳定下「2015年新增1.5萬輛新能源車」的目標,將統籌設立50億元人民幣(折合新台幣約254.25億元)的新能源汽車推廣應用扶持資金。   據《深圳市新能源汽車推廣應用若干政策措施》,深圳將對購買新能源汽車給予1:1配套地方補貼,且不逐年遞減。以純電動客車為例,每輛最高可補貼50萬元(折合新台幣約晝254.25萬元)(車身長度在10公尺以上),而純電動乘用車,標準工況續駛里程在250公里以上補貼則達到6萬元(折合新台幣約30.51萬)。   按照規定,對個人、企業購買使用新能源乘用車的補貼,主要用於機動車交通事故責任強制保險費、路橋費、充電費、自用充電設施及安裝費等方面,其中純電動乘用車最高補貼可達2萬元(折合新台幣約10.17萬元)(標準工況續駛里程在250公里以上)。   為鼓勵計程車運營企業購買使用純電動計程車,除了享受純電動乘用車購車和使用補貼外,對更新為純電動計程車的燃油計程車,另外給予推廣應用補貼5.58萬元(折合新台幣約28.37萬元)。計程車運營企業2015年到期更新為純電動計程車的,更新車輛數以同產權1:1比例置換,另給予置換數10%的純電動計程車指標獎勵。   在充電樁方面,截至目前,深圳累計建設快速充電站81座、慢速充電樁近3000個,但快速充電樁僅完成原計劃的三、四成,慢速充電樁只完成一成。而深圳市發改委明確表示,到2015年底前,深圳市將新增1800個快速充電樁。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

我家也有太陽能:預防鳥害是用戶責任 下了蛋就不能讓牠搬家

文:宋瑞文(加州能源特約撰述)

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

【其他文章推薦】

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

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

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

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

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

蟬聯兩年!Tesla Models S 再登美國年度最佳汽車

美國消費者報告(Consumer Reports)的調查顯示,電動車製造商 Tesla 的 Model S 已連續 2 年榮登年度最佳汽車。   消費者報告是根據道路表現、可靠度和防撞測試結果來評比車輛,除了選出整體表現最佳的汽車外,也會按照車種來評比。Model S 今年之所以蟬聯年度最佳汽車,原因除了充滿電後能跑 426 公里外,能夠透過網路升級軟體、及 Tesla 已克服初期面臨的技術問題,也是這輛車獲得青睞的原因。   在消費者報告劃分的十大分類中,只有 6 類是由日本車贏得最佳汽車頭銜,為消費者報告 19 年前開始評比以來最少,贏得最佳汽車頭銜的美國汽車品牌則有 3 個。除了 Tesla,另外 2 個品牌是 Buick 及雪佛蘭(Chevrolet)。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

基於flink和drools的實時日誌處理

1、背景

日誌系統接入的日誌種類多、格式複雜多樣,主流的有以下幾種日誌:

  • filebeat採集到的文本日誌,格式多樣
  • winbeat採集到的操作系統日誌
  • 設備上報到logstash的syslog日誌
  • 接入到kafka的業務日誌

以上通過各種渠道接入的日誌,存在2個主要的問題:

  • 格式不統一、不規範、標準化不夠
  • 如何從各類日誌中提取出用戶關心的指標,挖掘更多的業務價值

為了解決上面2個問題,我們基於flink和drools規則引擎做了實時的日誌處理服務。

2、系統架構

架構比較簡單,架構圖如下:

 

各類日誌都是通過kafka匯總,做日誌中轉。

flink消費kafka的數據,同時通過API調用拉取drools規則引擎,對日誌做解析處理后,將解析后的數據存儲到Elasticsearch中,用於日誌的搜索和分析等業務。

為了監控日誌解析的實時狀態,flink會將日誌處理的統計數據,如每分鐘處理的日誌量,每種日誌從各個機器IP來的日誌量寫到Redis中,用於監控統計。

3、模塊介紹

系統項目命名為eagle。

eagle-api:基於springboot,作為drools規則引擎的寫入和讀取API服務。

eagle-common:通用類模塊。

eagle-log:基於flink的日誌處理服務。

重點講一下eagle-log:

對接kafka、ES和Redis

對接kafka和ES都比較簡單,用的官方的connector(flink-connector-kafka-0.10和flink-connector-elasticsearch6),詳見代碼。

對接Redis,最開始用的是org.apache.bahir提供的redis connector,後來發現靈活度不夠,就使用了Jedis。

在將統計數據寫入redis的時候,最開始用的keyby分組后緩存了分組數據,在sink中做統計處理后寫入,參考代碼如下:

        String name = "redis-agg-log";
        DataStream<Tuple2<String, List<LogEntry>>> keyedStream = dataSource.keyBy((KeySelector<LogEntry, String>) log -> log.getIndex())
                .timeWindow(Time.seconds(windowTime)).trigger(new CountTriggerWithTimeout<>(windowCount, TimeCharacteristic.ProcessingTime))
                .process(new ProcessWindowFunction<LogEntry, Tuple2<String, List<LogEntry>>, String, TimeWindow>() {
                    @Override
                    public void process(String s, Context context, Iterable<LogEntry> iterable, Collector<Tuple2<String, List<LogEntry>>> collector) {
                        ArrayList<LogEntry> logs = Lists.newArrayList(iterable);
                        if (logs.size() > 0) {
                            collector.collect(new Tuple2(s, logs));
                        }
                    }
                }).setParallelism(redisSinkParallelism).name(name).uid(name);

後來發現這樣做對內存消耗比較大,其實不需要緩存整個分組的原始數據,只需要一個統計數據就OK了,優化后:

        String name = "redis-agg-log";
        DataStream<LogStatWindowResult> keyedStream = dataSource.keyBy((KeySelector<LogEntry, String>) log -> log.getIndex())
                .timeWindow(Time.seconds(windowTime))
                .trigger(new CountTriggerWithTimeout<>(windowCount, TimeCharacteristic.ProcessingTime))
                .aggregate(new LogStatAggregateFunction(), new LogStatWindowFunction())
                .setParallelism(redisSinkParallelism).name(name).uid(name);

這裏使用了flink的聚合函數和Accumulator,通過flink的agg操作做統計,減輕了內存消耗的壓力。

使用broadcast廣播drools規則引擎

1、drools規則流通過broadcast map state廣播出去。

2、kafka的數據流connect規則流處理日誌。

//廣播規則流
env.addSource(new RuleSourceFunction(ruleUrl)).name(ruleName).uid(ruleName).setParallelism(1)
                .broadcast(ruleStateDescriptor);

//kafka數據流
FlinkKafkaConsumer010<LogEntry> source = new FlinkKafkaConsumer010<>(kafkaTopic, new LogSchema(), properties);
env.addSource(source).name(kafkaTopic).uid(kafkaTopic).setParallelism(kafkaParallelism);
//數據流connect規則流處理日誌 BroadcastConnectedStream<LogEntry, RuleBase> connectedStreams = dataSource.connect(ruleSource); connectedStreams.process(new LogProcessFunction(ruleStateDescriptor, ruleBase)).setParallelism(processParallelism).name(name).uid(name);

具體細節參考開源代碼。

4、小結

本系統提供了一個基於flink的實時數據處理參考,對接了kafka、redis和elasticsearch,通過可配置的drools規則引擎,將數據處理邏輯配置化和動態化。

對於處理后的數據,也可以對接到其他sink,為其他各類業務平台提供數據的解析、清洗和標準化服務。

 

項目地址:

https://github.com/luxiaoxun/eagle

 

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

【其他文章推薦】

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

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

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

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

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

愛爾蘭擬課拿鐵稅 減少一次性咖啡杯

摘錄自2019年11月7日中央社報導

愛爾蘭氣候行動部長布魯敦(Richard Bruton)今(6日)表示,2021年前將對可拋式咖啡杯課徵所謂的「拿鐵稅」,試圖改變消費者習慣並削減使用一次性塑膠對環境的影響。

愛爾蘭去年連續第3年超出年度溫室氣體排放配額量後,開始對經濟採取削減環境影響的行動,盼透過徵收最高0.25歐元的擬議拿鐵稅,促進咖啡飲用者改攜帶環保杯,進一步推進都柏林在歐盟的法定承諾。

愛爾蘭最早於2002年推出塑膠袋稅,拿鐵稅則是為了鼓勵採取較永續行為而新增的賦稅之一。這類計畫還包括對超市櫃台販賣的較昂貴中量級塑膠袋增收0.25歐元稅金。

儘管某些商家已為攜帶環保杯的顧客打折,但去年當局資助的報告發現,愛爾蘭全國490萬人每年仍丟棄高達2億個一次性咖啡杯。

布魯敦說,因為零售業缺乏回收飲食包裝的基礎設備,可分解咖啡杯也會被課稅。

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

【其他文章推薦】

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

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

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

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

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

Golang 網絡編程

目錄

  • TCP網絡編程
  • UDP網絡編程
  • Http網絡編程
  • 理解函數是一等公民
  • HttpServer源碼閱讀
    • 註冊路由
    • 啟動服務
    • 處理請求
  • HttpClient源碼閱讀
    • DemoCode
    • 整理思路
    • 重要的struct
    • 流程
    • transport.dialConn
    • 發送請求

TCP網絡編程

存在的問題:

  • 拆包:
    • 對發送端來說應用程序寫入的數據遠大於socket緩衝區大小,不能一次性將這些數據發送到server端就會出現拆包的情況。
    • 通過網絡傳輸的數據包最大是1500字節,當TCP報文的長度 - TCP頭部的長度 > MSS(最大報文長度時)將會發生拆包,MSS一般長(1460~1480)字節。
  • 粘包:
    • 對發送端來說:應用程序發送的數據很小,遠小於socket的緩衝區的大小,導致一個數據包裏面有很多不通請求的數據。
    • 對接收端來說:接收數據的方法不能及時的讀取socket緩衝區中的數據,導致緩衝區中積壓了不同請求的數據。

解決方法:

  • 使用帶消息頭的協議,在消息頭中記錄數據的長度。
  • 使用定長的協議,每次讀取定長的內容,不夠的使用空格補齊。
  • 使用消息邊界,比如使用 \n 分隔 不同的消息。
  • 使用諸如 xml json protobuf這種複雜的協議。

實驗:使用自定義協議

整體的流程:

客戶端:發送端連接服務器,將要發送的數據通過編碼器編碼,發送。

服務端:啟動、監聽端口、接收連接、將連接放在協程中處理、通過解碼器解碼數據。

	//###########################
//######  Server端代碼  ###### 
//###########################

func main() {
	// 1. 監聽端口 2.accept連接 3.開goroutine處理連接
	listen, err := net.Listen("tcp", "0.0.0.0:9090")
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	for{
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("Fail listen.Accept : %v", err)
			continue
		}
		go ProcessConn(conn)
	}
}

// 處理網絡請求
func ProcessConn(conn net.Conn) {
	defer conn.Close()
	for  {
		bt,err:=coder.Decode(conn)
		if err != nil {
			fmt.Printf("Fail to decode error [%v]", err)
			return
		}
		s := string(bt)
		fmt.Printf("Read from conn:[%v]\n",s)
	}
}

//###########################
//######  Clinet端代碼  ###### 
//###########################
func main() {
	conn, err := net.Dial("tcp", ":9090")
	defer conn.Close()
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}

	// 將數據編碼併發送出去
	coder.Encode(conn,"hi server i am here");
}

//###########################
//######  編解碼器代碼  ###### 
//###########################
/**
 * 	解碼:
 */
func Decode(reader io.Reader) (bytes []byte, err error) {
	// 先把消息頭讀出來
	headerBuf := make([]byte, len(msgHeader))
	if _, err = io.ReadFull(reader, headerBuf); err != nil {
		fmt.Printf("Fail to read header from conn error:[%v]", err)
		return nil, err
	}
	// 檢驗消息頭
	if string(headerBuf) != msgHeader {
		err = errors.New("msgHeader error")
		return nil, err
	}
	// 讀取實際內容的長度
	lengthBuf := make([]byte, 4)
	if _, err = io.ReadFull(reader, lengthBuf); err != nil {
		return nil, err
	}
	contentLength := binary.BigEndian.Uint32(lengthBuf)
	contentBuf := make([]byte, contentLength)
	// 讀出消息體
	if _, err := io.ReadFull(reader, contentBuf); err != nil {
		return nil, err
	}
	return contentBuf, err
}

/**
 *  編碼
 *  定義消息的格式: msgHeader + contentLength + content
 *  conn 本身實現了 io.Writer 接口
 */
func Encode(conn io.Writer, content string) (err error) {
	// 寫入消息頭
	if err = binary.Write(conn, binary.BigEndian, []byte(msgHeader)); err != nil {
		fmt.Printf("Fail to write msgHeader to conn,err:[%v]", err)
	}
	// 寫入消息體長度
	contentLength := int32(len([]byte(content)))
	if err = binary.Write(conn, binary.BigEndian, contentLength); err != nil {
		fmt.Printf("Fail to write contentLength to conn,err:[%v]", err)
	}
	// 寫入消息
	if err = binary.Write(conn, binary.BigEndian, []byte(content)); err != nil {
		fmt.Printf("Fail to write content to conn,err:[%v]", err)
	}
	return err

客戶端的conn一直不被Close 有什麼表現?

四次揮手各個狀態的如下:

主從關閉方						被動關閉方
established					established
Fin-wait1					
										closeWait
Fin-wait2
Tiem-wait						lastAck
Closed							Closed

如果客戶端的連接手動的關閉,它和服務端的狀態會一直保持established建立連接中的狀態。

MacBook-Pro% netstat -aln | grep 9090
tcp4       0      0  127.0.0.1.9090         127.0.0.1.62348        ESTABLISHED
tcp4       0      0  127.0.0.1.62348        127.0.0.1.9090         ESTABLISHED
tcp46      0      0  *.9090                 *.*                    LISTEN

服務端的conn一直不被關閉 有什麼表現?

客戶端的進程結束后,會發送fin數據包給服務端,向服務端請求斷開連接。

服務端的conn不關閉的話,服務端就會停留在四次揮手的close_wait階段(我們不手動Close,服務端就任務還有數據/任務沒處理完,因此它不關閉)。

客戶端停留在 fin_wait2的階段(在這個階段等着服務端告訴自己可以真正斷開連接的消息)。

MacBook-Pro% netstat -aln | grep 9090
tcp4       0      0  127.0.0.1.9090         127.0.0.1.62888        CLOSE_WAIT
tcp4       0      0  127.0.0.1.62888        127.0.0.1.9090         FIN_WAIT_2
tcp46      0      0  *.9090                 *.*                    LISTEN

什麼是binary.BigEndian?什麼是binary.LittleEndian?

對計算機來說一切都是二進制的數據,BigEndian和LittleEndian描述的就是二進制數據的字節順序。計算機內部,小端序被廣泛應用於現代性 CPU 內部存儲數據;大端序常用於網絡傳輸和文件存儲。

比如:

一個數的二進製表示為 	 0x12345678
BigEndian   表示為: 0x12 0x34 0x56 0x78 
LittleEndian表示為: 0x78 0x56 0x34 0x12

UDP網絡編程

思路:

UDP服務器:1、監聽 2、循環讀取消息 3、回複數據。

UDP客戶端:1、連接服務器 2、發送消息 3、接收消息。

// ################################
// ######## UDPServer #########
// ################################
func main() {
	// 1. 監聽端口 2.accept連接 3.開goroutine處理連接
	listen, err := net.Listen("tcp", "0.0.0.0:9090")
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	for{
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("Fail listen.Accept : %v", err)
			continue
		}
		go ProcessConn(conn)
	}
}

// 處理網絡請求
func ProcessConn(conn net.Conn) {
	defer conn.Close()
	for  {
		bt,err:= coder.Decode(conn)
		if err != nil {
			fmt.Printf("Fail to decode error [%v]", err)
			return
		}
		s := string(bt)
		fmt.Printf("Read from conn:[%v]\n",s)
	}
}

// ################################
// ######## UDPClient #########
// ################################
func main() {

	udpConn, err := net.DialUDP("udp", nil, &net.UDPAddr{
		IP:   net.IPv4(127, 0, 0, 1),
		Port: 9091,
	})
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}

	_, err = udpConn.Write([]byte("i am udp client"))
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	bytes:=make([]byte,1024)
	num, addr, err := udpConn.ReadFromUDP(bytes)
	if err != nil {
		fmt.Printf("Fail to read from udp error: [%v]", err)
		return
	}
	fmt.Printf("Recieve from udp address:[%v], bytes:[%v], content:[%v]",addr,num,string(bytes))
}

Http網絡編程

思路整理:

HttpServer:1、創建路由器。2、為路由器綁定路由規則。3、創建服務器、監聽端口。 4啟動讀服務。

HttpClient: 1、創建連接池。2、創建客戶端,綁定連接池。3、發送請求。4、讀取響應。

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/login", doLogin)
	server := &http.Server{
		Addr:         ":8081",
		WriteTimeout: time.Second * 2,
		Handler:      mux,
	}
	log.Fatal(server.ListenAndServe())
}

func doLogin(writer http.ResponseWriter,req *http.Request){
	_, err := writer.Write([]byte("do login"))
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
}

HttpClient端

func main() {
	transport := &http.Transport{
    // 撥號的上下文
		DialContext: (&net.Dialer{
			Timeout:   30 * time.Second, // 撥號建立連接時的超時時間
			KeepAlive: 30 * time.Second, // 長連接存活的時間
		}).DialContext,
    // 最大空閑連接數
		MaxIdleConns:          100,  
    // 超過最大的空閑連接數的連接,經過 IdleConnTimeout時間後會失效
		IdleConnTimeout:       10 * time.Second, 
    // https使用了SSL安全證書,TSL是SSL的升級版
    // 當我們使用https時,這行配置生效
		TLSHandshakeTimeout:   10 * time.Second, 
		ExpectContinueTimeout: 1 * time.Second,  // 100-continue 狀態碼超時時間
	}

	// 創建客戶端
	client := &http.Client{
		Timeout:   time.Second * 10, //請求超時時間
		Transport: transport,
	}

	// 請求數據
	res, err := client.Get("http://localhost:8081/login")
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	defer res.Body.Close()

	bytes, err := ioutil.ReadAll(res.Body)
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	fmt.Printf("Read from http server res:[%v]", string(bytes))
}

理解函數是一等公民

點擊查看在github中函數相關的筆記

在golang中函數是一等公民,我們可以把一個函數當作普通變量一樣使用。

比如我們有個函數HelloHandle,我們可以直接使用它。

func HelloHandle(name string, age int) {
	fmt.Printf("name:[%v] age:[%v]", name, age)
}

func main() {
  HelloHandle("tom",12)
}

閉包

如何理解閉包:閉包本質上是一個函數,而且這個函數會引用它外部的變量,如下例子中的f3中的匿名函數本身就是一個閉包。 通常我們使用閉包起到一個適配的作用。

例1:

// f2是一個普通函數,有兩個入參數
func f2() {
	fmt.Printf("f2222")
}

// f1函數的入參是一個f2類型的函數
func f1(f2 func()) {
	f2()
}

func main() {
  // 由於golang中函數是一等公民,所以我們可以把f2同普通變量一般傳遞給f1
	f1(f2)
}

例2: 在上例中更進一步。f2有了自己的參數, 這時就不能直接把f2傳遞給f1了。

總不能傻傻的這樣吧f1(f2(1,2)) ???

而閉包就能解決這個問題。

// f2是一個普通函數,有兩個入參數
func f2(x int, y int) {
	fmt.Println("this is f2 start")
	fmt.Printf("x: %d y: %d \n", x, y)
	fmt.Println("this is f2 end")
}

// f1函數的入參是一個f2類型的函數
func f1(f2 func()) {
	fmt.Println("this is f1 will call f2")
	f2()
	fmt.Println("this is f1 finished call f2")
}

// 接受一個兩個參數的函數, 返回一個包裝函數
func f3(f func(int,int) ,x,y int) func() {
	fun := func() {
		f(x,y)
	}
	return fun
}

func main() {
	// 目標是實現如下的傳遞與調用
	f1(f3(f2,6,6))
}

實現方法的回調:

下面的例子中實現這樣的功能:就好像是我設計了一個框架,定好了整個框架運轉的流程(或者說是提供了一個編程模版),框架具體做事的函數你根據自己的需求自己實現,我的框架只是負責幫你回調你具體的方法。

// 自定義類型,handler本質上是一個函數
type HandlerFunc func(string, int)

// 閉包
func (f HandlerFunc) Serve(name string, age int) {
	f(name, age)
}

// 具體的處理函數
func HelloHandle(name string, age int) {
	fmt.Printf("name:[%v] age:[%v]", name, age)
}

func main() {
  // 把HelloHandle轉換進自定義的func中
	handlerFunc := HandlerFunc(HelloHandle)
  // 本質上會去回調HelloHandle方法
	handlerFunc.Serve("tom", 12)
  
  // 上面兩行效果 == 下面這行
  // 只不過上面的代碼是我在幫你回調,下面的是你自己主動調用
  HelloHandle("tom",12)
}

HttpServer源碼閱讀

註冊路由

直觀上看註冊路由這一步,就是它要做的就是將在路由器url pattern和開發者提供的func關聯起來。 很容易想到,它裏面很可能是通過map實現的。


func main() {
	// 創建路由器
	// 為路由器綁定路由規則
	mux := http.NewServeMux()
	mux.HandleFunc("/login", doLogin)
	...
}

func doLogin(writer http.ResponseWriter,req *http.Request){
	_, err := writer.Write([]byte("do login"))
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
}

姑且將ServeMux當作是路由器。我們使用http包下的 NewServerMux 函數創建一個新的路由器對象,進而使用它的HandleFunc(pattern,func)函數完成路由的註冊。

跟進NewServerMux函數,可以看到,它通過new函數返回給我們一個ServeMux結構體。

func NewServeMux() *ServeMux {
  return new(ServeMux) 
}

這個ServeMux結構體長下面這樣:在這個ServeMux結構體中我們就看到了這個維護pattern和func的map

type ServeMux struct {
	mu    sync.RWMutex 
	m     map[string]muxEntry
	hosts bool // whether any patterns contain hostnames
}

這個muxEntry長下面這樣:

type muxEntry struct {
	h       Handler
	pattern string
}

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

看到這裏問題就來了,上面我們手動註冊進路由器中的僅僅是一個有規定參數的方法,到這裏怎麼成了一個Handle了?我們也沒有說去手動的實現Handler這個接口,也沒有重寫ServeHTTP函數啊, 在golang中實現一個接口不得像下面這樣搞嗎?**

type Handle interface {
	Serve(string, int, string)
}

type HandleImpl struct {

}

func (h HandleImpl)Serve(string, int, string){

}

帶着這個疑問看下面的方法:

	// 由於函數是一等公民,故我們將doLogin函數同普通變量一樣當做入參傳遞進去。
 	mux.HandleFunc("/login", doLogin)

  func doLogin(writer http.ResponseWriter,req *http.Request){
    ...
	}

跟進去看 HandleFunc 函數的實現:

首先:HandleFunc函數的第二個參數是接收的函數的類型和doLogin函數的類型是一致的,所以doLogin能正常的傳遞進HandleFunc中。

其次:我們的關注點應該是下面的HandlerFunc(handler)

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
	mux.Handle(pattern, HandlerFunc(handler))
}

跟進這個HandlerFunc(handler) 看到下圖,真相就大白於天下了。golang以一種優雅的方式悄無聲息的為我們完成了一次適配。這麼看來上面的HandlerFunc(handler)並不是函數的調用,而是doLogin轉換成自定義類型。這個自定義類型去實現了Handle接口(因為它重寫了ServeHTTP函數)以閉包的形式完美的將我們的doLogin適配成了Handle類型。

在往下看Handle方法:

第一:將pattern和handler註冊進map中

第二:為了保證整個過程的併發安全,使用鎖保護整個過程。

// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()

	if pattern == "" {
		panic("http: invalid pattern")
	}
	if handler == nil {
		panic("http: nil handler")
	}
	if _, exist := mux.m[pattern]; exist {
		panic("http: multiple registrations for " + pattern)
	}

	if mux.m == nil {
		mux.m = make(map[string]muxEntry)
	}
	mux.m[pattern] = muxEntry{h: handler, pattern: pattern}

	if pattern[0] != '/' {
		mux.hosts = true
	}

啟動服務

概覽圖:

和java對比着看,在java一組複雜的邏輯會被封裝成一個class。在golang中對應的就是一組複雜的邏輯會被封裝成一個結構體。

對應HttpServer肯定也是這樣,http服務器在golang的實現中有自己的結構體。它就是http包下的Server。

它有一系列描述性屬性。如監聽的地址、寫超時時間、路由器。

	server := &http.Server{
		Addr:         ":8081",
		WriteTimeout: time.Second * 2,
		Handler:      mux,
	}
	log.Fatal(server.ListenAndServe())

我們看它啟動服務的函數:server.ListenAndServe()

實現的邏輯是使用net包下的Listen函數,獲取給定地址上的tcp連接。

再將這個tcp連接封裝進 tcpKeepAliveListenner 結構體中。

在將這個tcpKeepAliveListenner丟進Server的Serve函數中處理

// ListenAndServe 會監聽開發者給定網絡地址上的tcp連接,當有請求到來時,會調用Serve函數去處理這個連接。
// 它接收到所有連接都使用 TCP keep-alives相關的配置
// 
// 如果構造Server時沒有指定Addr,他就會使用默認值: “:http”
// 
// 當Server ShutDown或者是Close,ListenAndServe總是會返回一個非nil的error。
// 返回的這個Error是 ErrServerClosed
func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
  // 底層藉助於tcp實現
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

// tcpKeepAliveListener會為TCP設置一個keep-alive 超時時長。
// 它通常被 ListenAndServe 和 ListenAndServeTLS使用。
// 它保證了已經dead的TCP最終都會消失。
type tcpKeepAliveListener struct {
	*net.TCPListener
}

接着去看看Serve方法,上一個函數中獲取到了一個基於tcp的Listener,從這個Listener中可以不斷的獲取出新的連接,下面的方法中使用無限for循環完成這件事。conn獲取到后將連接封裝進httpConn,為了保證不阻塞下一個連接到到來,開啟新的goroutine處理這個http連接。

func (srv *Server) Serve(l net.Listener) error {
  // 如果有一個包裹了 srv 和 listener 的鈎子函數,就執行它
	if fn := testHookServerServe; fn != nil {
		fn(srv, l) // call hook with unwrapped listener
	}
	
  // 將tcp的Listener封裝進onceCloseListener,保證連接不會被關閉多次。
	l = &onceCloseListener{Listener: l}
	defer l.Close()
 
  // http2相關的配置
	if err := srv.setupHTTP2_Serve(); err != nil {
		return err
	}

	if !srv.trackListener(&l, true) {
		return ErrServerClosed
	}
	defer srv.trackListener(&l, false)
	
  // 如果沒有接收到請求睡眠多久
	var tempDelay time.Duration     // how long to sleep on accept failure
	baseCtx := context.Background() // base is always background, per Issue 16220
	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
  // 開啟無限循環,嘗試從Listenner中獲取連接。
	for {
		rw, e := l.Accept()
    // accpet過程中發生錯屋
		if e != nil {
			select {
        // 如果從server的doneChan中可以獲取內容,返回Server關閉了
			case <-srv.getDoneChan():
				return ErrServerClosed
			default:
			}
      // 如果發生了 net.Error 並且是臨時的錯誤就睡5毫秒,再發生錯誤睡眠的時間*2,上線是1s
			if ne, ok := e.(net.Error); ok && ne.Temporary() {
				if tempDelay == 0 {
					tempDelay = 5 * time.Millisecond
				} else {
					tempDelay *= 2
				}
				if max := 1 * time.Second; tempDelay > max {
					tempDelay = max
				}
				srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
				time.Sleep(tempDelay)
				continue
			}
			return e
		}
    // 如果沒有發生錯誤,清空睡眠的時間
		tempDelay = 0
    // 將接收到連接封裝進httpConn
		c := srv.newConn(rw)
		c.setState(c.rwc, StateNew) // before Serve can return
    // 開啟一條新的協程處理這個連接
		go c.serve(ctx)
	}
}

處理請求

c.serve(ctx)中就會去解析http相關的報文信息~,將http報文解析進Request結構體中。

部分代碼如下:

		// 將 server 包裹為 serverHandler 的實例,執行它的 ServeHTTP 方法,處理請求,返迴響應。
		// serverHandler 委託給 server 的 Handler 或者 DefaultServeMux(默認路由器)
		// 來處理 "OPTIONS *" 請求。
		serverHandler{c.server}.ServeHTTP(w, w.req)
// serverHandler delegates to either the server's Handler or
// DefaultServeMux and also handles "OPTIONS *" requests.
type serverHandler struct {
	srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
  // 如果沒有定義Handler就使用默認的
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}
	if req.RequestURI == "*" && req.Method == "OPTIONS" {
		handler = globalOptionsHandler{}
	}
  // 處理請求,返迴響應。
	handler.ServeHTTP(rw, req)
}

可以看到,req中包含了我們前面說的pattern,叫做RequestUri,有了它下一步就知道該回調ServeMux中的哪一個函數。

HttpClient源碼閱讀

DemoCode

func main() {
	// 創建連接池
	// 創建客戶端,綁定連接池
	// 發送請求
	// 讀取響應
	transport := &http.Transport{
		DialContext: (&net.Dialer{
			Timeout:   30 * time.Second, // 連接超時
			KeepAlive: 30 * time.Second, // 長連接存活的時間
		}).DialContext,
    // 最大空閑連接數
		MaxIdleConns:          100,             
    // 超過最大空閑連接數的連接會在IdleConnTimeout后被銷毀
		IdleConnTimeout:       10 * time.Second, 
		TLSHandshakeTimeout:   10 * time.Second, // tls握手超時時間
		ExpectContinueTimeout: 1 * time.Second,  // 100-continue 狀態碼超時時間
	}

	// 創建客戶端
	client := &http.Client{
		Timeout:   time.Second * 10, //請求超時時間
		Transport: transport,
	}

	// 請求數據,獲得響應
	res, err := client.Get("http://localhost:8081/login")
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	defer res.Body.Close()
  // 處理數據
	bytes, err := ioutil.ReadAll(res.Body)
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	fmt.Printf("Read from http server res:[%v]", string(bytes))
}

整理思路

http.Client的代碼其實是很多的,全部很細的過一遍肯定也會難度,下面可能也是只能提及其中的一部分。

首先明白一件事,我們編寫的HttpClient是在干什麼?(雖然這個問題很傻,但是總得問一下)是在發送Http請求。

一般我們在開發的時候,更多的編寫的是HttpServer的代碼。是在處理Http請求, 而不是去發送Http請求,Http請求都是是前端通過ajax經由瀏覽器發送到後端的。

其次,Http請求實際上是建立在tcp連接之上的,所以如果我們去看http.Client肯定能找到net.Dial("tcp",adds)相關的代碼。

那也就是說,我們要看看,http.Client是如何在和服務端建立連接、發送數據、接收數據的。

重要的struct

http.Client中有機幾個比較重要的struct,如下

http.Client結構體中封裝了和http請求相關的屬性,諸如 cookie,timeout,redirect以及Transport。

type Client struct {
	Transport RoundTripper
	CheckRedirect func(req *Request, via []*Request) error
	Jar CookieJar
	Timeout time.Duration
}

Tranport實現了RoundTrpper接口:

 type RoundTripper interface {   
  // 1、RoundTrip會去執行一個簡單的 Http Trancation,併為requestt返回一個響應
  // 2、RoundTrip不會嘗試去解析response
  // 3、注意:只要返回了Reponse,無論response的狀態碼是多少,RoundTrip返回的結果:err == nil 
  // 4、RoundTrip將請求發送出去后,如果他沒有獲取到response,他會返回一個非空的err。
  // 5、同樣,RoundTrip不會嘗試去解析諸如重定向、認證、cookie這種更高級的協議。 
  // 6、除了消費和關閉請求體之外,RoundTrip不會修改request的其他字段
  // 7、RoundTrip可以在一個單獨的gorountine中讀取request的部分字段。一直到ResponseBody關閉之前,調用者都不能取消,或者重用這個request
  // 8、RoundTrip始終會保證關閉Body(包含在發生err時)。根據實現的不同,在RoundTrip關閉前,關閉Body這件事可能會在一個單獨的goroutine中去做。這就意味着,如果調用者想將請求體用於後續的請求,必須等待知道發生Close
  // 9、請求的URL和Header字段必須是被初始化的。 
	RoundTrip(*Request) (*Response, error)
}

看上面RoundTrpper接口,它裏面只有一個方法RoundTrip,方法的作用就是執行一次Http請求,發送Request然後獲取Response。

RoundTrpper被設計成了一個支持併發的結構體。

Transport結構體如下:

type Transport struct {
	idleMu     sync.Mutex
   // user has requested to close all idle conns
	wantIdle   bool
  // Transport的作用就是用來建立一個連接,這個idleConn就是Transport維護的空閑連接池。
	idleConn   map[connectMethodKey][]*persistConn // most recently used at end
	idleConnCh map[connectMethodKey]chan *persistConn
}

其中的connectMethodKey也是結構體:

type connectMethodKey struct {
  // proxy 代理的URL,當他不為空時,就會一直使用這個key 
  // scheme 協議的類型, http https
  // addr 代理的url,也就是下游的url
	proxy, scheme, addr string
}

persistConn是一個具體的連接實例,包含連接的上下文。

type persistConn struct {
  // alt可選地指定TLS NextProto RoundTripper。 
  // 這用於今天的HTTP / 2和以後的將來的協議。 如果非零,則其餘字段未使用。
	alt RoundTripper
	t         *Transport
	cacheKey  connectMethodKey
	conn      net.Conn
	tlsState  *tls.ConnectionState
  // 用於從conn中讀取內容
	br        *bufio.Reader       // from conn
  // 用於往conn中寫內容
	bw        *bufio.Writer       // to conn
	nwrite    int64               // bytes written
  // 他是個chan,roundTrip會將readLoop中的內容寫入到reqch中
	reqch     chan requestAndChan 
  // 他是個chan,roundTrip會將writeLoop中的內容寫到writech中
	writech   chan writeRequest  
	closech   chan struct{}       // closed when conn closed

另外補充一個結構體:Request,他用來描述一次http請求的實例,它定義於http包request.go, 裏面封裝了對Http請求相關的屬性

type Request struct {
   Method string
   URL *url.URL
   Proto      string // "HTTP/1.0"
   ProtoMajor int    // 1
   ProtoMinor int    // 0
   Header Header
   Body io.ReadCloser
   GetBody func() (io.ReadCloser, error)
   ContentLength int64
   TransferEncoding []string
   Close bool
   Host string
   Form url.Values
   PostForm url.Values
   MultipartForm *multipart.Form
   Trailer Header
   RemoteAddr string
   RequestURI string
   TLS *tls.ConnectionState
   Cancel <-chan struct{}
   Response *Response
   ctx context.Context
}

這幾個結構體共同完成如下圖所示http.Client的工作流程

流程

我們想發送一次Http請求。首先我們需要構造一個Request,Request本質上是對Http協議的描述(因為大家使用的都是Http協議,所以將這個Request發送到HttpServer后,HttpServer能識別並解析它)。

// 從這行代碼開始往下看
	res, err := client.Get("http://localhost:8081/login")

// 跟進Get
	req, err := NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}
	return c.Do(req)

// 跟進Do
	func (c *Client) Do(req *Request) (*Response, error) {
	return c.do(req)
 } 

// 跟進do,do函數中有下面的邏輯,可以看到執行完send后已經拿到返回值了。所以我們得繼續跟進send方法
  if resp, didTimeout, err = c.send(req, deadline); err != nil 

// 跟進send方法,可以看到send中還有一send方法,入參分別是:request,tranpost,deadline
// 到現在為止,我們沒有看到有任何和服務端建立連接的動作發生,但是構造的req和擁有連接池的tranport已經見面了~
	resp, didTimeout, err = send(req, c.transport(), deadline)

// 繼續跟進這個send方法,看到了調用了rt的RoundTrip方法。
// 這個rt就是我們編寫HttpClient代碼時創建的,綁定在http.Client上的tranport實例。
// 這個RoundTrip方法的作用我們在上面已經說過了,最直接的作用就是:發送request 並獲取response。
	resp, err = rt.RoundTrip(req)

但是RoundTrip他是個定義在RoundTripper接口中的抽象方法,我們看代碼肯定是要去看具體的實現嘛
這裏可以使用斷點調試法:在上面最後一行上打上斷點,會進入到他的具體實現中。從圖中可以看到具體的實現在roundtrip中。

RoundTrip中調用的函數是我們自定義的transport的roundTrip函數, 跟進去如下:

緊接着我們需要一個conn,這個conn我們通過Transport可以獲取到。conn的類型為persistConn。

// roundTrip函數中又一個無限for循環
for {
    // 檢查請求的上下文是否關閉了
		select {
		case <-ctx.Done():
			req.closeBody()
			return nil, ctx.Err()
		default:
		}

    // 對傳遞進來的req進行了有一層的封裝,封裝后的這個treq可以被roundTrip修改,所以每次重試都會新建
		treq := &transportRequest{Request: req, trace: trace}
		cm, err := t.connectMethodForRequest(treq)
		if err != nil {
			req.closeBody()
			return nil, err
		}

    // 到這裏真的執行從tranport中獲取和對應主機的連接,這個連接可能是http、https、http代理、http代理的高速緩存, 但是無論如何我們都已經準備好了向這個連接發送treq
    // 這裏獲取出來的連接就是我們在上文中提及的persistConn
		pconn, err := t.getConn(treq, cm)
		if err != nil {
			t.setReqCanceler(req, nil)
			req.closeBody()
			return nil, err
		}

		var resp *Response
		if pconn.alt != nil {
			// HTTP/2 path.
			t.decHostConnCount(cm.key()) // don't count cached http2 conns toward conns per host
			t.setReqCanceler(req, nil)   // not cancelable with CancelRequest
			resp, err = pconn.alt.RoundTrip(req)
		} else {
      
      // 調用persistConn的roundTrip方法,發送treq並獲取響應。
			resp, err = pconn.roundTrip(treq)
		}
		if err == nil {
			return resp, nil
		}
		if !pconn.shouldRetryRequest(req, err) {
			// Issue 16465: return underlying net.Conn.Read error from peek,
			// as we've historically done.
			if e, ok := err.(transportReadFromServerError); ok {
				err = e.err
			}
			return nil, err
		}
		testHookRoundTripRetried()

		// Rewind the body if we're able to.  (HTTP/2 does this itself so we only
		// need to do it for HTTP/1.1 connections.)
		if req.GetBody != nil && pconn.alt == nil {
			newReq := *req
			var err error
			newReq.Body, err = req.GetBody()
			if err != nil {
				return nil, err
			}
			req = &newReq
		}
	}

整理思路:然後看上面代碼中獲取conn和roundTrip的實現細節。

我們需要一個conn,這個conn可以通過Transport獲取到。conn的類型為persistConn。但是不管怎麼樣,都得先獲取出 persistConn,才能進一步完成發送請求再得到服務端到響應。

然後關於這個persistConn結構體其實上面已經提及過了。重新貼在下面

type persistConn struct {
  // alt可選地指定TLS NextProto RoundTripper。 
  // 這用於今天的HTTP / 2和以後的將來的協議。 如果非零,則其餘字段未使用。
	alt RoundTripper
  
  conn      net.Conn
	t         *Transport
	br        *bufio.Reader  // 用於從conn中讀取內容
	bw        *bufio.Writer  // 用於往conn中寫內容
  // 他是個chan,roundTrip會將readLoop中的內容寫入到reqch中
	reqch     chan requestAndChan 
  // 他是個chan,roundTrip會將writeLoop中的內容寫到writech中
  
	nwrite    int64               // bytes written
	cacheKey  connectMethodKey
	tlsState  *tls.ConnectionState
	writech   chan writeRequest  
	closech   chan struct{}       // closed when conn closed

跟進 t.getConn(treq, cm)代碼如下:

	// 先嘗試從空閑緩衝池中取得連接
  // 所謂的空閑緩衝池就是Tranport結構體中的: idleConn map[connectMethodKey][]*persistConn 
  // 入參位置的cm如下:
  /* type connectMethod struct {
      // 代理的url,如果沒有代理的話,這個值為nil
			proxyURL     *url.URL 
			
			// 連接所使用的協議 http、https
			targetScheme string
      
	    // 如果proxyURL指定了http代理或者是https代理,並且使用的協議是http而不是https。
	    // 那麼下面的targetAddr就會不包含在connect method key中。
	    // 因為socket可以復用不同的targetAddr值
			targetAddr string
	}*/
	t.getIdleConn(cm);

	// 空閑緩衝池有的空閑連接的話返回conn,否則進行如下的select
	select {
    // todo 這裏我還不確定是在干什麼,目前猜測是這樣的:每個服務器能打開的socket句柄是有限的
    // 每次來獲取鏈接的時候,我們就計數+1。當整體的句柄在Host允許範圍內時我們不做任何干涉~
		case <-t.incHostConnCount(cmKey):
			// count below conn per host limit; proceed
    
    // 重新嘗試從空閑連接池中獲取連接,因為可能有的連接使用完后被放回連接池了
		case pc := <-t.getIdleConnCh(cm):
			if trace != nil && trace.GotConn != nil {
				trace.GotConn(httptrace.GotConnInfo{Conn: pc.conn, Reused: pc.isReused()})
			}
			return pc, nil
    // 請求是否被取消了
		case <-req.Cancel:
			return nil, errRequestCanceledConn
    // 請求的上下文是否Done掉了
		case <-req.Context().Done():
			return nil, req.Context().Err()
		case err := <-cancelc:
			if err == errRequestCanceled {
				err = errRequestCanceledConn
			}
			return nil, err
		}

	// 開啟新的gorountine新建連接一個連接
	go func() {
    /**
    *	新建連接,方法底層封裝了tcp client dial相關的邏輯
    *	conn, err := t.dial(ctx, "tcp", cm.addr())
    *	以及根據不同的targetScheme構建不同的request的邏輯。
    */
    // 獲取到persistConn
		pc, err := t.dialConn(ctx, cm)
    // 將persistConn寫到chan中
		dialc <- dialRes{pc, err}
	}()

	// 再嘗試從空閑連接池中獲取
  idleConnCh := t.getIdleConnCh(cm)
	select {
  // 如果上面的go協程撥號成功了,這裏就能取出值來
	case v := <-dialc:
		// Our dial finished.
		if v.pc != nil {
			if trace != nil && trace.GotConn != nil && v.pc.alt == nil {
				trace.GotConn(httptrace.GotConnInfo{Conn: v.pc.conn})
			}
			return v.pc, nil
		}
		// Our dial failed. See why to return a nicer error
		// value.
    // 將Host的連接-1
		t.decHostConnCount(cmKey)
		select {
    ...

transport.dialConn

下面代碼中的cm長這樣

// dialConn是Transprot的方法
// 入參:context上下文, connectMethod
// 出參:persisnConn
func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (*persistConn, error) {
	// 構建將要返回的 persistConn
  pconn := &persistConn{
		t:             t,
		cacheKey:      cm.key(),
		reqch:         make(chan requestAndChan, 1),
		writech:       make(chan writeRequest, 1),
		closech:       make(chan struct{}),
		writeErrCh:    make(chan error, 1),
		writeLoopDone: make(chan struct{}),
	}
	trace := httptrace.ContextClientTrace(ctx)
	wrapErr := func(err error) error {
		if cm.proxyURL != nil {
			// Return a typed error, per Issue 16997
			return &net.OpError{Op: "proxyconnect", Net: "tcp", Err: err}
		}
		return err
	}
  
  // 判斷cm中使用的協議是否是https
	if cm.scheme() == "https" && t.DialTLS != nil {
		var err error
		pconn.conn, err = t.DialTLS("tcp", cm.addr())
		if err != nil {
			return nil, wrapErr(err)
		}
		if pconn.conn == nil {
			return nil, wrapErr(errors.New("net/http: Transport.DialTLS returned (nil, nil)"))
		}
		if tc, ok := pconn.conn.(*tls.Conn); ok {
			// Handshake here, in case DialTLS didn't. TLSNextProto below
			// depends on it for knowing the connection state.
			if trace != nil && trace.TLSHandshakeStart != nil {
				trace.TLSHandshakeStart()
			}
			if err := tc.Handshake(); err != nil {
				go pconn.conn.Close()
				if trace != nil && trace.TLSHandshakeDone != nil {
					trace.TLSHandshakeDone(tls.ConnectionState{}, err)
				}
				return nil, err
			}
			cs := tc.ConnectionState()
			if trace != nil && trace.TLSHandshakeDone != nil {
				trace.TLSHandshakeDone(cs, nil)
			}
			pconn.tlsState = &cs
		}
	} else {
    // 如果不是https協議就來到這裏,使用tcp向httpserver撥號,獲取一個tcp連接。
		conn, err := t.dial(ctx, "tcp", cm.addr())
		if err != nil {
			return nil, wrapErr(err)
		}
    // 將獲取到tcp連接交給我們的persistConn維護
		pconn.conn = conn
    
    // 處理https相關邏輯
		if cm.scheme() == "https" {
			var firstTLSHost string
			if firstTLSHost, _, err = net.SplitHostPort(cm.addr()); err != nil {
				return nil, wrapErr(err)
			}
			if err = pconn.addTLS(firstTLSHost, trace); err != nil {
				return nil, wrapErr(err)
			}
		}
	}

	// Proxy setup.
	switch {
  // 如果代理URL為空,不做任何處理  
	case cm.proxyURL == nil:
		// Do nothing. Not using a proxy.
  //   
	case cm.proxyURL.Scheme == "socks5":
		conn := pconn.conn
		d := socksNewDialer("tcp", conn.RemoteAddr().String())
		if u := cm.proxyURL.User; u != nil {
			auth := &socksUsernamePassword{
				Username: u.Username(),
			}
			auth.Password, _ = u.Password()
			d.AuthMethods = []socksAuthMethod{
				socksAuthMethodNotRequired,
				socksAuthMethodUsernamePassword,
			}
			d.Authenticate = auth.Authenticate
		}
		if _, err := d.DialWithConn(ctx, conn, "tcp", cm.targetAddr); err != nil {
			conn.Close()
			return nil, err
		}
	case cm.targetScheme == "http":
		pconn.isProxy = true
		if pa := cm.proxyAuth(); pa != "" {
			pconn.mutateHeaderFunc = func(h Header) {
				h.Set("Proxy-Authorization", pa)
			}
		}
	case cm.targetScheme == "https":
		conn := pconn.conn
		hdr := t.ProxyConnectHeader
		if hdr == nil {
			hdr = make(Header)
		}
		connectReq := &Request{
			Method: "CONNECT",
			URL:    &url.URL{Opaque: cm.targetAddr},
			Host:   cm.targetAddr,
			Header: hdr,
		}
		if pa := cm.proxyAuth(); pa != "" {
			connectReq.Header.Set("Proxy-Authorization", pa)
		}
		connectReq.Write(conn)

		// Read response.
		// Okay to use and discard buffered reader here, because
		// TLS server will not speak until spoken to.
		br := bufio.NewReader(conn)
		resp, err := ReadResponse(br, connectReq)
		if err != nil {
			conn.Close()
			return nil, err
		}
		if resp.StatusCode != 200 {
			f := strings.SplitN(resp.Status, " ", 2)
			conn.Close()
			if len(f) < 2 {
				return nil, errors.New("unknown status code")
			}
			return nil, errors.New(f[1])
		}
	}

	if cm.proxyURL != nil && cm.targetScheme == "https" {
		if err := pconn.addTLS(cm.tlsHost(), trace); err != nil {
			return nil, err
		}
	}

	if s := pconn.tlsState; s != nil && s.NegotiatedProtocolIsMutual && s.NegotiatedProtocol != "" {
		if next, ok := t.TLSNextProto[s.NegotiatedProtocol]; ok {
			return &persistConn{alt: next(cm.targetAddr, pconn.conn.(*tls.Conn))}, nil
		}
	}

	if t.MaxConnsPerHost > 0 {
		pconn.conn = &connCloseListener{Conn: pconn.conn, t: t, cmKey: pconn.cacheKey}
	}
  
  // 初始化persistConn的bufferReader和bufferWriter
	pconn.br = bufio.NewReader(pconn) // 可以從上面給pconn維護的tcpConn中讀數據
	pconn.bw = bufio.NewWriter(persistConnWriter{pconn})// 可以往上面pconn維護的tcpConn中寫數據 
  
  // 新開啟兩條和persistConn相關的go協程。
	go pconn.readLoop()
	go pconn.writeLoop()
	return pconn, nil
}

上面的兩條goroutine 和 br bw共同完成如下圖的流程

發送請求

發送req的邏輯在http包的下的tranport包中的func (t *Transport) roundTrip(req *Request) (*Response, error) {}函數中。

如下:

	// 發送treq
	resp, err = pconn.roundTrip(treq)

	// 跟進roundTrip
  // 可以看到他將一個writeRequest結構體類型的實例寫入了writech中
	// 而這個writech會被上圖中的writeLoop消費,藉助bufferWriter寫入tcp連接中,完成往服務端數據的發送。
	pc.writech <- writeRequest{req, writeErrCh, continueCh}

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

【其他文章推薦】

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

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

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

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

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