分類: 3C資訊

  • 油耗高達6.1L 這款混合動力車不便宜點怎麼行

    油耗高達6.1L 這款混合動力車不便宜點怎麼行

    (使用2。5L發動機的凱美瑞雙擎工信部油耗也高達5。3升/100公里)。其它方面表現如何。介紹完動力之後,咱們再看看全新起亞K7的其它方面,外觀上比老款K7更加帥氣動感,精緻感要更加好,看起來更加像一款平民豪車。K7的尺寸為4970mm*1869mm*1470mm。

    各位朋友們,臨近年關,不知道現在你們還有沒有心情工作呢?

    廢話不說那麼多,今天小編美美又來給各位介紹新車啦,這一款新車就是起亞K7混合動力版。

    有的朋友可能對起亞K7非常的陌生,起亞在我們的印象中比較熟悉的車型就是智跑、K5、K3這一類的,其實起亞K7是起亞K系列的一款高端中級轎車。他的中文名叫做起亞凱尊,起亞凱尊是起亞的一款定位B+級的中級轎車產品,採用全進口的銷售方式,售價為24.3萬~32.46萬,雖然是進口身份,但是它24.3萬元的起價已經比定位中大型車的福特金牛座還要更加貴,所以在國內賣的不好也是意料之中。

    現款起亞凱尊↑↑↑

    但是這並不妨礙我們看一下起亞的這個混動軍團的又一款產品起亞K7混動的實力。

    新一代起亞K7於2016年1月在韓國亮相,至今正好一年的時間,而一年後的現在起亞發布了K7混動的消息,K7混動採用2.4升的自然吸氣發動機加38千瓦的電動機的動力總成。最大功率為157馬力,最大扭矩為206牛米。鋰電池的電池容量為1.76kwh。官方公布的百公里綜合油耗為6.1L/100km。

    現款凱尊的2.4L發動機↑↑↑

    目前關於這款2.4L的發動機並沒有太多的消息,是否是全新的發動機也是未知,但是參考其157馬力/206牛米的動力輸出來看,這樣的輸出水平比2013款凱尊2.4L發動機都要低了不少(2013款凱尊國五:173馬力/224牛米),因此小編猜測這是一台新發動機,並且採用了混動車型多半喜歡的阿特金森循環(這種發動機動力弱但是油耗表現優秀)。

    而38千瓦(52馬力)電機和現款起亞K5混動的電機功率相同,因此電機可能是和現款K5採用相同的38千瓦(52馬力)/205牛米輸出的電機。因此發動機和電動機的總輸出功率可能為225馬力/429牛米。

    為什麼綜合油耗偏高?

    相比較同級別的雅閣混動、君越混動來說,K7的6.1升的綜合油耗是偏高的表現了,君越混動採用1.8L發動機+114千瓦電機的動力系統,工信部油耗為4.7升/100公里,雅閣採用2.0L發動機+135千瓦電機的動力系統,工信部油耗為4.2升/100公里。

    那為什麼會有偏高的油耗呢?由於K7電機的功率偏弱,因此電機主要的工作區間為低速區間,因此需要使用大排量發動機來彌補中段加速的動力,因此K7使用了2.4L的發動機,較大的發動機排量也會導致油耗偏高。(使用2.5L發動機的凱美瑞雙擎工信部油耗也高達5.3升/100公里)。

    其它方面表現如何?

    介紹完動力之後,咱們再看看全新起亞K7的其它方面,外觀上比老款K7更加帥氣動感,精緻感要更加好,看起來更加像一款平民豪車。K7的尺寸為4970mm*1869mm*1470mm。軸距為2855mm,這樣的尺寸其實已經是逼近一台中大型轎車了。

    內飾上K7也是以穩重設計為主,畢竟是起亞的高端價位的車型,現款K7的內飾做工質感在同價位就是比較出色的了,因此對於全新的K7的內飾質感小編也絲毫不擔心。而且得益於較大的車身尺寸,全新K7的內部空間貌似還比較寬敞。

    動力上,除了混動的K7之外,還會有2.0T和2.4L發動機的車型,在美國市場上K7還有6缸3.3L發動機,最大功率290馬力,並且匹配8AT變速箱,不過不用想了,即使3.3L車型來到國內,受限於稅率相信也沒多少人會買的。

    售價多少?

    回到混動K7上,看完了混動K7的數據之後相信大家會好奇,這款起亞K7混動會賣多少錢呢?綜合目前國產的起亞K5混動售價為19.98萬-24.88萬,比K5更加高級並且採用進口身份的K7售價也會更加高,但是在前不久上市的起亞極睿卻給我們帶來了極具震撼的性價比;因此作為起亞的混動新生代車型,K7混動價格預計也不會貴到離譜,因此小編猜測K7混動的價格會在23.98萬左右,你們覺得這個價格如何呢?

    競爭對手:

    本田雅閣混動

    23.98-27.98萬

    相比起亞來說本田的品牌在中國的認可度更加高,而更加低的綜合油耗以及技術上的優勢都是雅閣混動的優勢所在,也是K7混動要面對的。

    起亞K5混動

    19.98萬-24.88萬

    為什麼說K5混動是K7混動的對手呢?在許多人看來,K5和K7同為中級車,但是K7要貴得多,因此選擇K5也就更加划算了,而對於選擇K7混動的人來說,為什麼不選擇一個同樣大空間低油耗的中級車K5呢?

    別克君越混動

    27.58-30.58萬

    國人對於別克的喜愛是十分明顯的,君越高級的底盤質感以及配置上的優勢都是同價位比較明顯的,而君越混動的油耗也比K7混動更加低,品質感也絲毫不遜色於K7混動,但是君越混動的價格偏高也是一個問題,如果K7混動也定這個價的話,那麼它倆就可以正面廝殺了。

    總結:

    其實無論是對於哪個品牌來說,受限於日益嚴格的排放法規,混動都是接下來的主戰場,而從起亞極睿的上市來看,技術偏弱的起亞選擇了爭取時間優勢,率先以低價入市的方式佔領市場打開口碑,之後有了良好的市場基礎,也就不用懼怕其它車型的競爭,因此低價入市的戰略應該是起亞的主要思路,這個角度來看,起亞K7的價格還是十分值得期待的。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

    ※超省錢租車方案

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

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

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

  • 成天喊着要SUV 這幾款只要5萬起你買不買?

    成天喊着要SUV 這幾款只要5萬起你買不買?

    值得一提的是,甚至還能在它身上看到換擋撥片。動力方面,1。5L自然吸氣發動機加5MT的搭配,性能方面不用擔心,完全夠用。至於另外一台AMT變速箱,畢竟受限於成本,性能方面肯定不及CVT,但相比那些只有純手動擋的對手來說也算得上一優勢。

    叫獸有時候總在想,SUV究竟還要火多久?是不是今後就要與傳統轎車“平分秋色”了?

    我不得而知。總之,存在即合理。叫獸可不是來探討這個高深的問題的,既然老老少少都對SUV抱有這麼大的熱情,倒不如成人之美,今天就推薦幾款市面上能買到(或即將上市)價格最親民並且還很靠譜的SUV,喜歡SUV的同學們可不要錯過。

    以下按價格由低到高的順序排列。

    江淮汽車 – 瑞風S2mini

    預售價:4.58 – 5.68萬

    瑞風S2mini的推出,更是將SUV的火爆演繹到了極致。這款定位在小型SUV – 瑞風S2之下的S2mini,幾乎是今後市面上能買到的最便宜的大廠SUV。

    正如它的造型一樣,呆萌的造型只賣獃萌的價格。作為江淮最入門的車型,瑞風S2mini同樣運用了江淮最新的家族設計,我們依然能在這個“小巧”的前臉上看到“寶瓶式”設計,看上去在呆萌中透露出一絲狠勁,挺好。

    外掛式備胎算是S2mini的一大亮點,從後方看去就像一位背着書包的壞小子,竟毫無違和感;3775*1685*1775mm的車身也能體現其“小學生”的身份。

    內飾倒是讓人眼前一亮,紅黑色的搭配彰顯了年輕人的活力,頂配車型甚至還採用了皮質包裹,對於這麼一款售價的SUV來說,有點意思。

    動力則搭載一台1.3L自然吸氣發動機,最大馬力99ps,峰值扭矩為126Nm,這樣的參數對於這麼一個“小身材”綽綽有餘;與發動機匹配的只有一台5MT變速箱。

    叫獸點評:升高几厘米底盤,冠上SUV稱號的S2mini未來的市場一定會比同價位轎車快活得多。

    長城汽車 – 哈弗H1

    指導價:5.49 – 7.89萬

    H6銷量的恐怖程度相信大家早有耳聞,叫獸的印象中,國內月銷量超8萬的單一車型除了五菱宏光就是H6了。作為哈弗家族的入門產品,H1的表現如何呢?

    你可以認為H1就是從M4拉皮而來,但不可否認的是,H1擁有同級SUV里較高的顏值。碩大的蜂窩式黑色格柵,看上去頗有幾分氣勢。

    車尾設計精緻幹練,如果說S2mini是個調皮的小子,那H1更像位體育委員。別多想,這排氣孔只是裝飾,唬人的。

    內飾給人的驚喜很大,雖然有些某豪華品牌的設計元素在裏面,不過從視覺層面來看還是挺讓人滿意的。值得一提的是,甚至還能在它身上看到換擋撥片。

    動力方面,1.5L自然吸氣發動機加5MT的搭配,性能方面不用擔心,完全夠用;至於另外一台AMT變速箱,畢竟受限於成本,性能方面肯定不及CVT,但相比那些只有純手動擋的對手來說也算得上一優勢。

    叫獸點評:長城的功底還是不錯的,更推薦手動版本。

    長安汽車 – CS15

    指導價:5.79 – 7.79萬

    近幾年,長安的“CS系列”死磕長城哈弗,雖然整體銷量還有些距離,但也算混的順風順水。CS15則是很有力的證明。

    CS15擁有比以上兩款更接近SUV的造型,造型上採用高度原創的設計,看上去也更討喜。

    內飾部分同樣令人滿意,看上去似乎亮點不多,不過長安紮實的做工和用料還是對得起它的價格的。

    除了同樣是1.5L + 5MT的動力組合外,CS15最大的看點在於它是同級車型里鮮有配備了雙離合變速箱的車型。換擋表現不敢說多麼平順,但比起AMT變速箱還是有明顯優勢的。這一點也能讓其吸引更多潛在客戶的關注。

    叫獸點評:6、7萬的自動擋SUV,非CS15莫屬。

    上汽通用五菱 – 寶駿510

    預售價:5.98 – 7.58萬

    510這款車不用多說,自去年廣州車展亮相后就受到了極大關注,不難預料,這即將成為寶駿又一爆款車型。

    天馬行空的車頭設計不用多介紹了,叫獸認為這個設計起碼向上越了兩級,這也將成為510未來競爭的最大殺手鐧。

    內飾部分,叫獸認為510是以上幾款車裡最值得表揚的。無論是設計還是用料均達到了主流水平,重點是,這是原創設計喲!

    動力搭載的是與730同款的L2B 1.5L自然吸氣發動機,性能和可靠性自然無須擔心。唯一遺憾的是,先期並沒有提供自動擋版本。

    叫獸點評:近兩年寶駿推出的車型幾乎都成為爆款,不用多想,510又要革了小型SUV市場的命。

    THE

    END

    總結

    誰說便宜無好貨?當工業發展到一定程度,成本和設計都成熟以後,依然能推出物美價廉的產品。如果你對SUV情有獨鍾並且預算有限,這幾款是個非常不錯的選擇。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

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

  • 5-10萬預算還想買7座?這些車是全民認證的好!

    5-10萬預算還想買7座?這些車是全民認證的好!

    08-9。28萬寶駿730的銷量雖然沒有宏光那麼硬氣,但是2016年全年370169輛的銷量也足以讓競爭對手汗顏了,寶駿730的車身尺寸為4685/4710*1785*1715mm,軸距為2750mm。提供5座、7座、8座版車型。雖然中控台依然是採用的硬塑料,中高配依然配備中控大屏幕,但是730的內飾設計明星要比宏光時尚很多,看起來也比較有設計感,畢竟一分價錢一分貨。

    在5-10萬元這個價位,如果單論哪種車子的實用性最高,小編肯定會不假思索的推薦MpV(微面),因為這些車子價格非常實惠,質量很好,空間很大,拉人載貨都非常方便。所以小編就給大家推薦幾款2016年全年銷量比較好的MpV。

    上汽通用五菱-五菱宏光

    指導價:4.18-6.98萬

    其實第一不用說大家就猜到是五菱宏光,中華神車五菱宏光在2016年12月份的銷量高達82543輛,2016年全年累計銷量為650018輛,這樣高調的銷量數據估計只有美國的皮卡福特F-150可以媲美了。雖然五菱宏光的發動機沒法和大排量V8的F-150相比,但是宏光價格要便宜很多啊!而且,宏光可以盡情的甩尾漂移,這點F-150不好做到吧!畢竟宏光也是征服過秋名山的选手。

    宏光的不同車型的車身尺寸略有差異,但是軸距都為2720mm,座位數為5-8個不等。當然低配的宏光是不可能有中控大屏幕的,畢竟車價較低,不過,有了中控大屏,宏光的中控看起來立馬高端很多。

    宏光的動力系統為1.2L 86馬力/1.5L 112馬力+5擋手動,別看宏光馬力較小,但是宏光的定位是微面,就是用來干粗活的,所以即使是滿載,只要油門轟起來,也不會感到動力不足。

    上汽通用五菱-寶駿730

    指導價:6.08-9.28萬

    寶駿730的銷量雖然沒有宏光那麼硬氣,但是2016年全年370169輛的銷量也足以讓競爭對手汗顏了,寶駿730的車身尺寸為4685/4710*1785*1715mm,軸距為2750mm。提供5座、7座、8座版車型。

    雖然中控台依然是採用的硬塑料,中高配依然配備中控大屏幕,但是730的內飾設計明星要比宏光時尚很多,看起來也比較有設計感,畢竟一分價錢一分貨。

    寶駿730的動力選擇很豐富,有自吸、同時也有渦輪發動機,甚至還有AMT車型可供消費者選擇。其實小編認為如果只是偶爾拉貨,多數情況是坐人的話,那麼最好還是選擇730吧!畢竟730前置前驅結構,噪音要比宏光小很多,同時整車的質感和配置也要明顯強於宏光,多花兩萬元還是比較值得的!

    長安汽車-歐尚

    指導價:5.19-6.49萬

    長安歐尚2016年全年的銷量為118185輛,表現還算不錯,歐尚的車身尺寸為4465*1725*1685/1700mm,軸距為2680mm。全系均為七座車型。

    內飾的造型絕對對得起這個價位,全黑的中控看起來質感很不錯,甚至有種比較高檔的感覺,不過內飾的用料肯定是以硬塑料為主了,畢竟價位有限么,可以理解。

    其實歐尚的性價比挺不錯的,絕對不比730的性價比低,畢竟6.49萬的歐尚就有ESp、上坡輔助、電動天窗、真皮方向盤、前/后駐車雷達、倒車視頻影像、仿皮座椅、GpS導航、中控大屏、藍牙、後座出風口等配置,關鍵是歐尚的優惠比730要大不少,如果不介意歐尚的尺寸小於730的話,那麼歐尚還是很值得入手的。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

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

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

  • 別再寫一摞if-else了!再寫開除!兩種設計模式帶你消滅它!

    別再寫一摞if-else了!再寫開除!兩種設計模式帶你消滅它!

    題外話:本來不想解釋、可是看完評論,有點服氣。沒想到居然這麼多人能曲解題意。這篇文章明顯是在說,不要寫一大堆if-else,一大堆是啥意思很難懂嗎?我沒有一句話說了不要寫if-else。開頭也給出了具體需求,在這種需求的前提下不要寫if-else,沒毛病吧??

    代碼潔癖狂們!看到一個類中有幾十個if-else是不是很抓狂?
    設計模式學了用不上嗎?面試的時候問你,你只能回答最簡單的單例模式,問你有沒有用過反射之類的高級特性,回答也是否嗎?
    這次就讓設計模式(模板方法模式+工廠模式)和反射助你消滅if-else!
    真的是開發中超超超超超超有用的乾貨啊!

    那個坑貨

    某日,碼農胖滾豬接到上級一個需求,這個需求牛逼了,一站式智能報表查詢平台,支持mysql、pgxl、tidb、hive、presto、mongo等眾多數據源,想要啥數據都能通通給你查出來展示,對於業務人員數據分析有重大意義!

    雖然各個數據源的參數校驗、查詢引擎和查詢邏輯都不一樣,但是胖滾豬對這些框架都很熟悉,這個難不倒她,她只花了一天時間就都寫完了。

    領導胖滾熊也對胖滾豬的效率表示了肯定。可是好景不長,第三天,領導閑着沒事,準備做一下code review,可把胖滾熊驚呆了,一個類裏面有近30個if-else代碼,我滴個媽呀,這可讓代碼潔癖狂崩潰了。

    // 檢驗入參合法性
    Boolean check = false;
    if(DataSourceEnum.hive.equals(dataSource)){
        check = checkHiveParams(params);
    } else if(DataSourceEnum.tidb.equals(dataSource)){
        check = checkTidbParams(params);
    } else if(DataSourceEnum.mysql.equals(dataSource)){
        check = checkMysqlParams(params);
    } // else if ....... 省略pgxl、presto等
    if(check){
        if(DataSourceEnum.hive.equals(dataSource)){
            list = queryHive(params);
        } else if(DataSourceEnum.tidb.equals(dataSource)){
            list = queryTidb(params);
        } else if(DataSourceEnum.mysql.equals(dataSource)){
            list = queryMysql(params);
        } // else if ....... 省略pgxl、presto等
    }
    //記錄日誌
    log.info("用戶={} 查詢數據源={} 結果size={}",params.getUserName(),params.getDataSource(),list.size());
    

    模板模式來救場

    首先我們來分析下,不管是什麼數據源,算法結構(流程)都是一樣的,1、校驗參數合法性 2、查詢 3、記錄日誌。這不就是說模板一樣、只不過具體細節不一樣,沒錯吧?

    讓我們來看看設計模式中模板方法模式的定義吧:

    模板方法模式:定義一個操作中的算法的框架,而將一些步驟延遲到子類中. 使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。通俗的講,就是將子類相同的方法, 都放到其抽象父類中。

    我們這需求不就和模板方法模式差不多嗎?因此我們可以把模板抽到父類(抽象類)中。至於特定的步驟實現不一樣,這些特殊步驟,由子類去重寫就好了。

    廢話不多說了,我們先把父類模板寫好吧,完全一樣的邏輯是記錄日誌,這步在模板寫死就好。至於檢驗參數和查詢,這兩個方法各不相同,因此需要置為抽象方法,由子類去重寫。

    public abstract class AbstractDataSourceProcesser <T extends QueryInputDomain> {
        public List<HashMap> query(T params){
            List<HashMap> list = new ArrayList<>();
            //檢驗參數合法性 不同的引擎sql校驗邏輯不一樣
            Boolean b = checkParam(params);
            if(b){
                //查詢
                list = queryData(params);
            }
            //記錄日誌
            log.info("用戶={} 查詢數據源={} 結果size={}",params.getUserName(),params.getDataSource(),list.size());
            return list;
        }
        //抽象方法 由子類來實現特定邏輯
        abstract Boolean checkParam(T params);
        abstract List<HashMap> queryData(T params);
    }
    

    這段代碼非常簡單。但是為了照顧新手,還是想解釋一個東西:

    T這個玩意。叫泛型,因為不同數據源的入參不一樣,所以我們使用泛型。但是他們也有公共的參數,比如用戶名。因此為了不重複冗餘,更好的利用公共資源,在泛型的設計上,我們可以有一個泛型上限,<T extends QueryInputDomain>

    public class QueryInputDomain<T> {
        public String userName;//查詢用戶名
        public String dataSource;//查詢數據源 比如mysql\tidb等
        public T params;//特定的參數 不同的數據源參數一般不一樣
    }
    public class MysqlQueryInput extends QueryInputDomain{
        private String database;//數據庫
        public String sql;//sql
    }
    
    

    接下來就輪到子類出場了,通過上面的分析,其實也很簡單了,不過是繼承父類,重寫checkParam()和queryData()方法,下面以mysql數據源為例,其他數據源也都一樣的套路:

    @Component("dataSourceProcessor#mysql")
    public class MysqlProcesser extends AbstractDataSourceProcesser<MysqlQueryInput>{
        @Override
        public Boolean checkParam(MysqlQueryInput params) {
            System.out.println("檢驗mysql參數是否準確");
            return true;
        }
    
        @Override
        public List<HashMap> queryData(MysqlQueryInput params) {
            List<HashMap> list = new ArrayList<>();
            System.out.println("開始查詢mysql數據");
            return list;
        }
    }
    

    這樣一來,所有的數據源,都自成一體,擁有一個只屬於自己的類,後續要擴展數據源、或者要修改某個數據源的邏輯,都非常方便和清晰了。

    說實話,模板方法模式太簡單了,抽象類這東西也太基礎普遍了,一般應屆生都會知道的。但是對於初入職場的新人來說,還真不太能果斷應用在實際生產中。因此提醒各位:一定要有一個抽象思維,避免代碼冗餘重複。

    另外,要再啰嗦幾句,即使工作有幾年的工程師也很容易犯一個錯誤。就是把思維局限在今天的需求,比如老闆一開始只給你一個mysql數據源查詢的需求,壓根沒有if-else,可能你就不會放在心上,直接在一個類中寫死,不會考慮到後續的擴展。直到後面越來越多的新需求,你才恍然大悟,要全部重構一番,這樣浪費自己的時間了。因此提醒各位:做需求不要局限於今天,要考慮到未來。 從一開始就做到高擴展性,後續需求變更和維護就非常爽了。

    原創聲明:本文為【胖滾豬學編程】原創博文,轉載請註明出處。以漫畫形式讓編程生動有趣!原創不易,求關注!

    工廠模式來救場

    但是模板模式還是沒有完全解決胖滾豬的if-else,因為需要根據傳進來的dataSource參數,判斷由哪個service來實現查詢邏輯,現在是這麼寫的:

      if(DataSourceEnum.hive.equals(dataSource)){
            list = queryHive(params);
        } else if(DataSourceEnum.tidb.equals(dataSource)){
            list = queryTidb(params);
        }
    
    

    那麼這種if-else應該怎麼去幹掉呢?我想先跟你講講工廠模式的那些故事。

    工廠模式:工廠方法模式是一種創建對象的模式,它被廣泛應用在jdk中以及Spring和Struts框架中。它將創建對象的工作轉移到了工廠類。

    為了呼應一下工廠兩字,我特意舉一個代工廠的例子讓你理解,這樣你應該會有更深刻的印象。

    以手機製造業為例。我們知道有蘋果手機、小米手機等等,每種品牌的手機製造方法必然不相同,我們可以先定義好一個手機標準接口,這個接口有make()方法,然後不同型號的手機都繼承這個接口:

    #Phone類:手機標準規範類(AbstractProduct)
    public interface Phone {
        void make();
    }
    #MiPhone類:製造小米手機(Product1)
    public class MiPhone implements Phone {
        public MiPhone() {
            this.make();
        }
        @Override
        public void make() {
            System.out.println("make xiaomi phone!");
        }
    }
    #IPhone類:製造蘋果手機(Product2)
    public class IPhone implements Phone {
        public IPhone() {
            this.make();
        }
        @Override
        public void make() {
            System.out.println("make iphone!");
        }
    }
    
    

    現在有某手機代工廠:【天霸手機代工廠】。客戶只會告訴該工廠手機型號,就要匹配到不同型號的製作方案,那麼代工廠是怎麼實現的呢?其實也很簡單,簡單工廠模式(還有抽象工廠模式和工廠方法模式,有興趣可以了解下)是這麼實現的:

    #PhoneFactory類:手機代工廠(Factory)
    public class PhoneFactory {
        public Phone makePhone(String phoneType) {
            if(phoneType.equalsIgnoreCase("MiPhone")){
                return new MiPhone();
            }
            else if(phoneType.equalsIgnoreCase("iPhone")) {
                return new IPhone();
            }
        }
    }
    
    

    這樣客戶告訴你手機型號,你就可以調用代工廠類的方法去獲取到對應的手機製造類。你會發現其實也不過是if-else,但是把if-else抽到一個工廠類,由工廠類統一創建對象,對我們的業務代碼無入侵,不管是維護還是美觀上都會好很多。

    首先,我們應該在每個特定的dataSourceProcessor(數據源執行器),比如MysqlProcesser、TidbProcesser中添加spring容器註解@Component。該註解我想應該不用多解釋了吧~重點是:我們可以把不同數據源都搞成類似的bean name,形如dataSourceProcessor#數據源名稱,如下兩段代碼:

    @Component("dataSourceProcessor#mysql")
    public class MysqlProcesser extends AbstractDataSourceProcesser<MysqlQueryInput>{
    @Component("dataSourceProcessor#tidb")
    public class TidbProcesser extends AbstractDataSourceProcesser<TidbQueryInput>{
    

    這樣有什麼好處呢?我可以利用Spring幫我們一次性加載出所有繼承於AbstractDataSourceProcesser的Bean ,形如Map<String, AbstractDataSourceProcesser>,Key是Bean的名稱、而Value則是對應的Bean:

    @Service
    public class QueryDataServiceImpl implements QueryDataService {
        @Resource
        public Map<String, AbstractDataSourceProcesser> dataSourceProcesserMap;
        public static String beanPrefix = "dataSourceProcessor#";
        @Override
        public List<HashMap> queryData(QueryInputDomain domain) {
            AbstractDataSourceProcesser dataSourceProcesser = dataSourceProcesserMap.get(beanPrefix + domain.getDataSource());
            //省略query代碼
        }
    }
    
    

    可能你還是不太理解,我們直接看一下運行效果:

    1、dataSourceProcesserMap內容如下所示,存儲了所有數據源Bean,Key是Bean的名稱、而Value則是對應的Bean:

    2、我只需要通過key(即前綴+數據源名稱=beanName),就能匹配到對應的執行器了。比如當參數dataSource為tidb的時候,key為dataSourceProcessor#tidb,根據key可以直接從dataSourceProcesserMap中獲取到TidbProcesser

    public static String classPrefix = "com.lyl.java.advance.service.";
    
    AbstractDataSourceProcesser sourceGenerator = 
    (AbstractDataSourceProcesser) Class.forName
    (classPrefix+DataSourceEnum.getClasszByCode(domain.getDataSource()))
    .newInstance();
    
    

    需要注意的是,該種方法是通過className來獲取到類的實例,而前端傳參肯定是不會傳className過來的。因此可以用到枚舉類,去定義好不同數據源的類名:

    public enum DataSourceEnum {
        mysql("mysql", "MysqlProcesser"),
        tidb("tidb", "TidbProcesser");
        private String code;
        private String classz;
    

    原創聲明:本文為【胖滾豬學編程】原創博文,轉載請註明出處。以漫畫形式讓編程生動有趣!原創不易,求關注!

    總結

    有些童鞋總覺得設計模式用不上,因為平時寫代碼除了CRUD還是CRUD,面試的時候問你設計模式,你只能回答最簡單的單例模式,問你有沒有用過反射之類的高級特性,回答也是否。

    其實不然,JAVA這23種設計模式,每一個都是經典。今天我們就用模板方法模式+工廠模式(或者反射)解決了讓人崩潰的if-else。後續對於設計模式的學習,也應該多去實踐,從真實的項目中找到用武之地,你才算真正把知識佔為己有了。

    本篇文章的內容和技術點雖然很簡單,但旨在告訴大家應該要有一個很好的代碼抽象思維。杜絕在代碼中出現一大摞if-else或者其他爛代碼。

    即使你有很好的代碼抽象思維,做需求開發的時候,也不要局限於當下,只考慮現在,要多想想未來的擴展性。

    就像你談戀愛一樣,只考慮當下的是渣男,考慮到未來的,才算是一個負責任的人

    “願世界沒有渣男”

    原創聲明:本文為【胖滾豬學編程】原創博文,轉載請註明出處。以漫畫形式讓編程生動有趣!原創不易,求關注!

    本文來源於公眾號:【胖滾豬學編程】。一枚集顏值與才華於一身,不算聰明卻足夠努力的女程序媛。用漫畫形式讓編程so easy and interesting!求關注!

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

    【其他文章推薦】

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

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

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

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

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

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

  • 【實戰】基於OpenCV的水表字符識別(OCR)

    【實戰】基於OpenCV的水表字符識別(OCR)

    目錄

    • 1. USB攝像頭取圖
    • 2. 圖像預處理:獲取屏幕ROI
      • 2.1. 分離提取屏幕區域
      • 2.2. 計算屏幕區域的旋轉角度
      • 2.3. 裁剪屏幕區域
      • 2.4. 旋轉圖像至正向視角
      • 2.5. 提取文字圖像
      • 2.6. 封裝上述過程
    • 3. 字符分割,獲取單個字符的圖像
    • 4. 模板匹配:確定字符內容
      • 4.1. make_template
      • 4.2. 模板修復
      • 4.3. 重新加載模板數據
      • 4.4. 模板匹配

    1. USB攝像頭取圖

    由於分辨率越高,處理的像素就越多,導致分析圖像的時間變長,這裏,我們設定攝像頭的取圖像素為(240,320):

    cap = cv2.VideoCapture(0)  # 根據電腦連接的情況填入攝像頭序號
    assert cap.isOpened()
    
    # 以下設置显示屏的寬高
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)
    cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter.fourcc('M', 'J', 'P', 'G'))
    

    這裏提幾個常用的標準分辨率:

    • VGA (Video Graphics Array): 640×480
    • QVGA (QuarterVGA): 240×320
    • QQVGA: 120×160

    接下來可以捕獲一幀數據看一下狀態:

    # %% 捕獲一幀清晰的圖像
    def try_frame():
        while True:
            ret, im_frame = cap.read()
            cv2.imshow("frame", im_frame)  # 显示圖像
    
            # im_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # 可選擇轉換為灰度圖
            if cv2.waitKey(1) & 0xFF == ord('q'):
                break
    
        cv2.destroyAllWindows()
        return im_frame
    
    im_frame = try_frame()
    env.imshow(im_frame)
    

    ps: 鏡頭角度會存在一定的歪斜,沒有關係,我們後面會進行處理。

    2. 圖像預處理:獲取屏幕ROI

    利用屏幕的亮度,通過簡單的閾值操作和輪廓操作,獲取屏幕輪廓,然後將圖像角度校正,最後獲得正向的文字內容。

    2.1. 分離提取屏幕區域

    通過OTSU的閾值化操作,將圖像處理為二值狀態。這個很重要,因為如果直接使用彩圖或灰度圖,會由於外部光線的變化,導致後期字符匹配時整體灰度值與模板的差別而降低置信度,導致較大的誤差。而二值圖可以避免這個問題。

    然後利用開運算(白底黑字,如果黑底白字則為閉運算),消除噪點。

    im_latest = try_frame()
    im_gray = mvlib.color.rgb2gray(image)
    im_bin = mvlib.filters.threshold(im_gray, invert=False)
    # im_erosion = mvlib.morphology.erosion(im_bin, (11, 11))
    # im_dilation = mvlib.morphology.dilation(im_erosion, (5, 5))
    im_opening = mvlib.morphology.opening(im_bin, (11, 11))
    env.imshow(im_opening)
    

    2.2. 計算屏幕區域的旋轉角度

    提取圖像的最大輪廓,然後獲取其包絡矩形。

    list_cnts = mvlib.contours.find_cnts(im_opening)
    if len(list_cnts) != 1:
        print(f"非唯一輪廓,請通過面積篩選過濾")
        # assert 0
        cnts_sorted = mvlib.contours.cnts_sort(list_cnts, mvlib.contours.cnt_area)
        list_cnts = [cnts_sorted[0]]
    
    box, results = mvlib.contours.approx_rect(list_cnts[0], True)
    angle = results[2]  # 此處的角度是向逆時針傾斜,記作:-4
    if abs(angle) > 45:
        angle = (angle + 45) % 90 - 45
    print(angle, box)
    

    上述過程輸出:

    1.432098388671875
    [[282 173]
     [ 29 167]
     [ 32  41]
     [285  47]]
    

    2.3. 裁剪屏幕區域

    至此可以丟棄im_opening以及im_bin的圖像了。我們重新回到im_gray上進行操作(需要重新進行閾值化以獲取文字的二值圖)。

    list_width = box[:,0]
    list_height= box[:,1]
    w_min, w_max = min(list_width), max(list_width)
    h_min, h_max = min(list_height), max(list_height)
    
    im_screen = im_gray[h_min:h_max, w_min:w_max]
    env.imshow(im_screen)
    

    2.4. 旋轉圖像至正向視角

    im_screen_orthogonal = mvlib.transform.rotate(im_screen, angle, False)
    # env.imshow(im_screen_orthogonal)
    im_screen_core = im_screen_orthogonal[20:-20, 20:-20]
    env.imshow(im_screen_core)
    

    2.5. 提取文字圖像

    第二次執行閾值化操作,但這一次是在屏幕內部,排除了屏幕外複雜的背景后,可以很容易的獲取到文字的內容。由於我們只關心数字,所以通過閉運算將細體字過濾掉。

    im_core_bin = mvlib.filters.threshold(im_screen_core, invert=False)
    im_closing = mvlib.morphology.closing(im_core_bin, (3,3))
    env.imshow(im_closing)
    

    2.6. 封裝上述過程

    瑣碎的預處理過程就告一段落了,我們可以將上述的內容封裝成一個簡單的函數:

    def preprocess():
        # 獲取屏幕區域
        im_latest = try_frame()
        ...
        im_closing = mvlib.morphology.closing(im_core_bin, (3,3))
        return im_closing
    

    3. 字符分割,獲取單個字符的圖像

    字符分割,一方面是製作模板的需要(當然,你也可以直接用畫圖工具裁剪出一張模板圖像);另一方面是為了加速模板匹配的效率。當然,你完全可以在整張圖像上利用 match_template() 查找模板,但如果進行多模板匹配,重複的掃描整張圖像,效率就大打折扣了。

    先提供完整的代碼

    char_width_min = 7
    gap_height_max = 5
    
    def segment_chars(im_core):
        list_char_img = []
        # 字符區域
        raw_bkg = np.all(im_core, axis=0)
        col_bkg = np.all(im_core, axis=1)
    
        # 計算字高
        ndarr_char_height = np.where(False == col_bkg)[0]
        char_height_start = ndarr_char_height[0]
        item_last = ndarr_char_height[0]
        for item in ndarr_char_height:
            if item - item_last > gap_height_max:
                char_height_start = item
            item_last = item
        char_height_end = ndarr_char_height[-1] +1
        print(f"字高【{char_height_end - char_height_start}】")
    
        ndarr_chars_pos = np.where(False == raw_bkg)[0]
        ndarr_chars_pos = np.append(ndarr_chars_pos,
                                    im_core.shape[1] + char_width_min)
    
        last_idx = ndarr_chars_pos[0]
        curr_char_width = 1
        for curr_idx in ndarr_chars_pos:
            idx_diff = curr_idx - last_idx
            # 這裏應該限制最小寬度>=2,否則認為是一個粘連字
            if idx_diff <= 2:
                curr_char_width += idx_diff
            else:  # 新的字符
                char_width_end = last_idx +1
                char_width_start = char_width_end - curr_char_width
                im_char_last = im_core[char_height_start:char_height_end,
                                    char_width_start:char_width_end]
                list_char_img.append(im_char_last)
                curr_char_width = 0
            last_idx = curr_idx
        return list_char_img
    

    按照行列,獲取圖像中的文字像素點集:

    raw_bkg = np.all(im_core, axis=0)
    col_bkg = np.all(im_core, axis=1)
    

    由此,可以知道255(黑色)的區域從大約 39 到 75,那麼 75 - 29 = 36 就是字高。

    另外,圖像中有可能存在噪點,去掉就是了(我這裏只是簡單粗暴的處理下,請見諒)。

    行的處理同樣。如果發現間隔,那麼就可以分離字符。最後,輸出每個字符的圖像。

    檢驗下效果:

    list_char_imgs = segment_chars(im_core)
    env.imshow(list_char_imgs[1])
    

    4. 模板匹配:確定字符內容

    利用模板匹配,實現字符識別的過程。這裏不再細說OpenCV的 cv2.matchTemplate() 函數,只描述應用過程。

    4.1. make_template

    首先,有必要把字符先作為模板存儲下來。

    def make_tpls(list_tpl_imgs, dir_save, dict_tpl=None):
        if not dict_tpl:
            dict_tpl = {}
    
        str_items = input("請輸入模板上的文本內容,用於校對(例如215801): ")
    
        assert len(str_items) == len(list_tpl_imgs)
        for i, v in enumerate(str_items):
            filename = v
            if v in dict_tpl:
                filename = v + "_" + str(random.random())
            else:
                dict_tpl[v] = list_tpl_imgs[i]
            path_save = os.path.join(dir_save, filename + ".jpg")
            mvlib.io.imsave(path_save, list_tpl_imgs[i])
    
        return dict_tpl
    

    這裏,同一字符有必要多存儲幾張,最後擇優(或者一個字符通過多個模板匹配的結果來確定)。

    4.2. 模板修復

    這個過程,雖然沒啥子技術含量,但卻對結果影響很大。在前一步驟中,我們每一個字符都收集了多張模板圖像。現在,從中擇優錄取。還有,可以手動編輯模板的圖片,去除模板多餘的白邊(邊並不是文字內容的一部分,而且會降低字符的匹配度)。

    4.3. 重新加載模板數據

    def load_saved_tpls(dir_tpl):
        saved_tpls = os.listdir(dir_tpl)
    
        dict_tpl = {}  # {"1": imread("mvdev/tmp/tpl/1.jpg"), ...}
        for i in saved_tpls:
            filename = os.path.splitext(i)[0]
            path_tpl = os.path.join(dir_tpl, i)
    
            im_rgb = cv2.imread(path_tpl)
            im_gray = mvlib.color.rgb2gray(im_rgb)
            dict_tpl[filename] = im_gray
        return dict_tpl
    
    dir_tpl = "tpl/"
    dict_tpls = load_saved_tpls(dir_tpl)
    

    4.4. 模板匹配

    def number_ocr_matching(im_char):
        most_likely = [1, ""]
        for key, im_tpl in dict_tpls.items():
            try:
                pos, similarity = mvlib.feature.match_template(im_char, im_tpl, way="most")
                if similarity < most_likely[0]:
                    most_likely = [similarity, key]
            except:
                im_char_old = im_char.copy()
                h = max(im_char.shape[0], im_tpl.shape[0])
                w = max(im_char.shape[1], im_tpl.shape[1])
                im_char = np.ones((h,w), dtype="uint8") * 255
                # im_char2 = mvlib.pixel.bitwise_and(z, im_char)
                im_char[:im_char_old.shape[0], :im_char_old.shape[1]] = im_char_old
    
                pos, similarity = mvlib.feature.match_template(im_char, im_tpl, way="most")
                if similarity < most_likely[0]:
                    most_likely = [similarity, key]
    
        print(f"字符識別為【{most_likely[1]}】相似度【{most_likely[0]}】")
        return most_likely[1]
    
    def application(list_char_imgs):
        str_ocr = ""
        for im_char in list_char_imgs:
            width_img = im_char.shape[1]
            # 判斷字符
            match_char = number_ocr_matching(im_char)
            str_ocr += match_char
        return str_ocr
    
    str_ocr2 = application(list_char_imgs)
    print(str_ocr2)
    

    過程中,opencv出現了報錯,是由於模板的shape大於當前分割字符的shape。這個很正常,採集圖像時由於距離的微調(注意,距離變化不能太大,OpenCV的默認算子不支持模板縮放)可能導致字符尺寸更小。解決方案也很簡單,直接把字符圖像拓展到大於模板的狀態就OK了。

    額,忘了刪除debug信息了……再來一次~

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

    【其他文章推薦】

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

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

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

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

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

  • 最近學習了限流相關的算法

    最近學習了限流相關的算法

    最近測試team在測試過程中反饋部分接口需要做一定的限流措施,剛好我也回顧了下限流相關的算法。常見限流相關的算法有四種:計數器算法, 滑動窗口算法, 漏桶算法, 令牌桶算法

    1.計數器算法(固定窗口)

     計數器算法是使用計數器在周期內累加訪問次數,當達到設定的閾值時就會觸發限流策略。下一個周期開始時,清零重新開始計數。此算法在單機和分佈式環境下實現都非常簡單,可以使用Redis的incr原子自增和線程安全即可以實現

     這個算法常用於QPS限流和統計訪問總量,對於秒級以上周期來說會存在非常嚴重的問題,那就是臨界問題,如下圖:

     假設我們設置的限流策略時1分鐘限制計數100,在第一個周期最後5秒和第二個周期的開始5秒,分別計數都是88,即在10秒時間內計數達到了176次,已經遠遠超過之前設置的閾值,由此可見,計數器算法(固定窗口)限流方式對於周期比較長的限流存在很大弊端。

     Java 實現計數器(固定窗口):

    package com.brian.limit;
    
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.AtomicInteger;
    
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * 固定窗口
     */
    @Slf4j
    public class FixWindow {
    
        private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
    
        private final int limit = 100;
    
        private AtomicInteger currentCircleRequestCount = new AtomicInteger(0);
    
        private AtomicInteger timeCircle = new AtomicInteger(0);
    
        private void doFixWindow() {
            scheduledExecutorService.scheduleWithFixedDelay(() -> {
                log.info(" 當前時間窗口,第 {} 秒 ", timeCircle.get());
                if(timeCircle.get() >= 60) {
                    timeCircle.set(0);
                    currentCircleRequestCount.set(0);
                    log.info(" =====進入新的時間窗口===== ");
                }
                if(currentCircleRequestCount.get() > limit) {
                    log.info("觸發限流策略,當前窗口累計請求數 : {}", currentCircleRequestCount);
                } else {
                    final int requestCount = (int) ((Math.random() * 5) + 1);
                    log.info("當前發出的 ==requestCount== : {}", requestCount);
                    currentCircleRequestCount.addAndGet(requestCount);
                }
               timeCircle.incrementAndGet();
            }, 0, 1, TimeUnit.SECONDS);
        }
    
        public static void main(String[] args) {
            new FixWindow().doFixWindow();
        }
        
    }

    2.滑動窗口算法

     滑動窗口算法是將時間周期拆分成N個小的時間周期,分別記錄小周期裏面的訪問次數,並且根據時間的滑動刪除過期的小周期。如下圖,假設時間周期為1分鐘,將1分鐘再分為2個小周期,統計每個小周期的訪問數量,則可以看到,第一個時間周期內,訪問數量為92,第二個時間周期內,訪問數量為104,超過100的訪問則被限流掉了。

     

     由此可見,當滑動窗口的格子劃分的越多,那麼滑動窗口的滾動就越平滑,限流的統計就會越精確。此算法可以很好的解決固定窗口算法的臨界問題。

      Java實現滑動窗口:

    package com.brian.limit;
    
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.AtomicInteger;
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * 滑動窗口
     * 
     * 60s限流100次請求
     */
    @Slf4j
    public class RollingWindow {
    
        private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
    
        // 窗口跨度時間60s
        private int timeWindow = 60;
    
        // 限流100個請求
        private final int limit = 100;
    
        // 當前窗口請求數
        private AtomicInteger currentWindowRequestCount = new AtomicInteger(0);
    
        // 時間片段滾動次數
        private AtomicInteger timeCircle = new AtomicInteger(0);
    
        // 觸發了限流策略后等待的時間
        private AtomicInteger waitTime = new AtomicInteger(0);
    
        // 在下一個窗口時,需要減去的請求數
        private int expiredRequest = 0;
    
        // 時間片段為5秒,每5秒統計下過去60秒的請求次數
        private final int slidingTime = 5;
    
        private ArrayBlockingQueue<Integer> slidingTimeValues = new ArrayBlockingQueue<>(11);
    
        public void rollingWindow() {
            scheduledExecutorService.scheduleWithFixedDelay(() -> {
    
                if (waitTime.get() > 0) {
                    waitTime.compareAndExchange(waitTime.get(), waitTime.get() - slidingTime);
                    log.info("=====當前滑動窗口===== 限流等待下一個時間窗口倒計時: {}s", waitTime.get());
                    if (currentWindowRequestCount.get() > 0) {
                        currentWindowRequestCount.set(0);
                    }
                } else {
                    final int requestCount = (int) ((Math.random() * 10) + 7);
                    if (timeCircle.get() < 12) {
                        timeCircle.incrementAndGet();
                    }
                    
                log.info("當前時間片段5秒內的請求數: {} ", requestCount);
                currentWindowRequestCount.addAndGet(requestCount);
                log.info("=====當前滑動窗口===== {}s 內請求數: {} ", timeCircle.get()*slidingTime , currentWindowRequestCount.get());
    
                if(!slidingTimeValues.offer(requestCount)){
                    expiredRequest =  slidingTimeValues.poll();
                    slidingTimeValues.offer(requestCount);
                } 
    
                if(currentWindowRequestCount.get() > limit) {
                    // 觸發限流
                    log.info("=====當前滑動窗口===== 請求數超過100, 觸發限流,等待下一個時間窗口 ");
                    waitTime.set(timeWindow);
                    timeCircle.set(0);
                    slidingTimeValues.clear();
                } else {
                    // 沒有觸發限流,滑動下一個窗口需要,移除相應的:在下一個窗口時,需要減去的請求數
                    log.info("=====當前滑動窗口===== 請求數 <100, 未觸發限流,當前窗口請求總數: {},即將過期的請求數:{}"
                            ,currentWindowRequestCount.get(), expiredRequest);
                    currentWindowRequestCount.compareAndExchange(currentWindowRequestCount.get(), currentWindowRequestCount.get() - expiredRequest);
                }
            }   
            }, 5, 5, TimeUnit.SECONDS);
        }
    
        public static void main(String[] args) {
            new RollingWindow().rollingWindow();
        }
        
    
    }

    計數器(固定窗口)和滑動窗口區別:

    計數器算法是最簡單的算法,可以看成是滑動窗口的低精度實現。滑動窗口由於需要存儲多份的計數器(每一個格子存一份),所以滑動窗口在實現上需要更多的存儲空間。也就是說,如果滑動窗口的精度越高,需要的存儲空間就越大。

    3.漏桶算法

     漏桶算法是訪問請求到達時直接放入漏桶,如當前容量已達到上限(限流值),則進行丟棄(觸發限流策略)。漏桶以固定的速率進行釋放訪問請求(即請求通過),直到漏桶為空。

     Java實現漏桶:

    package com.brian.limit;
    
    import java.util.concurrent.*;
    
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * 漏桶算法
     */
    @Slf4j
    public class LeakyBucket {
        private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
    
        // 桶容量
        public  int capacity = 1000;
        
        // 當前桶中請求數
        public int curretRequest = 0;
    
        // 每秒恆定處理的請求數
        private final int handleRequest = 100;
    
        public void doLimit() {
            scheduledExecutorService.scheduleWithFixedDelay(() -> {
                final int requestCount = (int) ((Math.random() * 200) + 50);
                if(capacity > requestCount){
                    capacity -= requestCount;
                    log.info("<><>當前1秒內的請求數:{}, 桶的容量:{}", requestCount, capacity);
                    if(capacity <=0) {
                        log.info(" =====觸發限流策略===== ");
                    } else {
                        capacity += handleRequest;
                        log.info("<><><><>當前1秒內處理請求數:{}, 桶的容量:{}", handleRequest, capacity);
                    }
                } else {
                    log.info("<><><><>當前請求數:{}, 桶的容量:{},丟棄的請求數:{}", requestCount, capacity,requestCount-capacity);
                    if(capacity <= requestCount) {
                        capacity = 0;
                    }
                    capacity += handleRequest;
                    log.info("<><><><>當前1秒內處理請求數:{}, 桶的容量:{}", handleRequest, capacity);
                }
            }, 0, 1, TimeUnit.SECONDS);
        }
    
        public static void main(String[] args) {
            new LeakyBucket().doLimit();
        }
    }

     漏桶算法有個缺點:如果桶的容量過大,突發請求時也會對後面請求的接口造成很大的壓力。

    4.令牌桶算法

     令牌桶算法是程序以恆定的速度向令牌桶中增加令牌,令牌桶滿了之後會丟棄新進入的令牌,當請求到達時向令牌桶請求令牌,如獲取到令牌則通過請求,否則觸發限流策略。

     

     Java實現令牌桶:

    package com.brian.limit;
    
    import java.util.concurrent.*;
    
    import lombok.extern.slf4j.Slf4j;
    /**
     * 令牌桶算法
     */
    @Slf4j
    public class TokenBucket {
        private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
    
        // 桶容量
        public  int capacity = 1000;
        
        // 當前桶中請求數
        public int curretToken = 0;
    
        // 恆定的速率放入令牌
        private final int tokenCount = 200;
    
        public void doLimit() {
            scheduledExecutorService.scheduleWithFixedDelay(() -> {
                
                new Thread( () -> {
                    if(curretToken >= capacity) {
                        log.info(" =====桶中的令牌已經滿了===== ");
                        curretToken = capacity;
                    } else {
                        if((curretToken+tokenCount) >= capacity){
                          log.info(" 當前桶中的令牌數:{},新進入的令牌將被丟棄的數: {}",curretToken,(curretToken+tokenCount-capacity));
                          curretToken = capacity;
                      } else {
                          curretToken += tokenCount;
                      }
                    }
                }).start();
    
                new Thread( () -> {
                    final int requestCount = (int) ((Math.random() * 200) + 50);
                    if(requestCount >= curretToken){
                        log.info(" 當前請求數:{},桶中令牌數: {},將被丟棄的請求數:{}",requestCount,curretToken,(requestCount - curretToken));
                        curretToken = 0;
                    } else {
                        log.info(" 當前請求數:{},桶中令牌數: {}",requestCount,curretToken);
                        curretToken -= requestCount;
                    }
                }).start();
            }, 0, 500, TimeUnit.MILLISECONDS);
        }
    
        public static void main(String[] args) {
            new TokenBucket().doLimit();
        }
        
    }

    漏桶算法和令牌桶算法區別:

    令牌桶可以用來保護自己,主要用來對調用者頻率進行限流,為的是讓自己不被打垮。所以如果自己本身有處理能力的時候,如果流量突發(實際消費能力強於配置的流量限制),那麼實際處理速率可以超過配置的限制。而漏桶算法,這是用來保護他人,也就是保護他所調用的系統。主要場景是,當調用的第三方系統本身沒有保護機制,或者有流量限制的時候,我們的調用速度不能超過他的限制,由於我們不能更改第三方系統,所以只有在主調方控制。這個時候,即使流量突發,也必須捨棄。因為消費能力是第三方決定的。
    總結起來:如果要讓自己的系統不被打垮,用令牌桶。如果保證被別人的系統不被打垮,用漏桶算法

     

    參考博客:https://blog.csdn.net/weixin_41846320/article/details/95941361

         https://www.cnblogs.com/xuwc/p/9123078.html

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

    【其他文章推薦】

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

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

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

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

    ※回頭車貨運收費標準

  • Nginx 的變量究竟是怎麼一回事?

    之前說了很多關於 Nginx 模塊的內容,還有一部分非常重要的內容,那就是 Nginx 的變量。變量在 Nginx 中可以說無處不在,認識了解這些變量的作用和原理同樣是必要的,下面幾乎囊括了關於 Nginx 的所有變量,單獨看起來可能比較枯燥,放心,後面依然有實戰內容。

    Nginx 變量的運行原理

    圍繞 Nginx 中的變量模塊可以分為兩類,一類是提供變量的模塊,另外一類是使用變量的模塊。

    • 提供變量的模塊
      • 在 Preconfiguration 源代碼中定義變量名以及可以解析出變量的方法
    • 使用變量的模塊
      • 解析 nginx.conf 時定義變量的使用方式

    也就是在 Nginx 啟動時,已經定義了變量,而只有當真正處理請求的時候,才會根據 nginx.conf 解析出來的變量使用方式調用 Preconfiguration 中定義的方法來實際獲取值。

    這也是變量的兩個特性:

    • 惰性求值:只有使用的時候才會去調方法解析
    • 變量值可以時刻變化,其值為使用的那一時刻的值。例如發送響應包體字節數,實際在發送的過程中是一直在變化的。

    除了 Nginx 的模塊之外,Nginx 框架也包含許多的變量,這些變量不需要通過編譯模塊來引入,而且,Nginx 框架所提供的變量往往反映了處理請求的細節,因此,了解 Nginx 框架所提供的變量是十分有必要的。

    HTTP 請求相關的變量

    先來看一下關於 HTTP 請求的相關變量。

    • arg_參數名:URL 中某個具體參數的值

    • query_string:與 args 變量完全相同

    • args:全部 URL 參數

    • is_args:如果請求 URL 中有參數則返回 ?,否則返回空

    • content_length:HTTP 請求中標識包體長度的 Content-Length 頭部的值。如果請求中沒有攜帶這個參數,那麼就取不到對應的值。

    • content_type:標識請求包體類型的 Content-Type 頭部的值。同樣需要用戶請求中攜帶對應的參數。

    • uri:請求的 URI(不同於 URL,不包括 ? 后的參數)

    • document_uri:與 uri 完全相同。由於歷史原因而存在的。

    • request_uri:請求的 URL(包括 URI 以及完整的參數)

    • scheme:協議名,例如 HTTP 或者 HTTPS

    • request_method:請求方法,例如 GET 或者 POST

    • request_length:所有請求內容的大小,包括請求行、頭部、包體等

    • remote_user:由 HTTP Basic Authentication 協議傳入的用戶名

    • request_body_file:很多時候會將用戶請求的包體存放到文件中,這個變量就是臨時存放請求包體的文件

      • 如果包體非常小則不會存文件
      • client_body_in_file_only 指令強制所有包體存入文件,且可決定是否刪除
    • request_body:請求中的包體,這個變量當且僅當使用反向代理,且設定用內存暫存包體時才有效

    • request:原始的 URL 請求,含有方法與協議版本,例如 GET /?a=1&b=22 HTTP/1.1

    • host

      • 先從請求行中獲取
      • 如果含有 Host 頭部,則用其值替換掉請求行中的主機名
      • 如果前兩者都取不到,則使用匹配上的 server_name
    • http_頭部名字:返回一個具體請求頭部的值

      特殊變量,這些變量會做一些處理。

      • http_host
      • http_user_agent
      • http_referer
      • http_via
      • http_x_forwarded_for
      • http_cookie

      通用變量,除了以上的變量,都可以取到對應的值。

    TCP 連接相關的變量

    下面是關於 TCP 連接的變量。

    • binary_remote_addr:客戶端地址的整形格式,對於 IPv4 是 4 字節,對於 IPv6 是 16 字節,所以在 limit_req 和 limit_conn 中通常可以用作 key (詳見:Nginx 處理 HTTP 請求的 11 個階段 中的 preaccess 階段)
    • connection:遞增的連接序號
    • connection_requests:當前連接上執行過的請求數,對 keepalive 連接有意義
    • remote_addr:客戶端地址
    • remote_port:客戶端端口
    • proxy_protocol_addr:若使用了 proxy_protocol 協議,則返回協議中的地址,否則返回空
    • proxy_protocol_port:若使用了 proxy_protocol 協議則返回協議中的端口,否則返回空
    • server_addr:服務端地址
    • server_port:服務器端端口
    • TCP_INFO:TCP 內核層參數,包括 $tcpinfo_rtt, ​$tcpinfo_rttvar,​$tcpinfo_snd_cwnd, $tcpinfo_rcv_space
    • server_protocol:服務器端協議,例如 HTTP/1.1

    Nginx 處理請求過程中產生的變量

    Nginx 處理 HTTP 請求的過程中也會產生很多變量。

    • request_time:請求處理到現在的耗時,單位為秒,精確到毫秒
    • server_name:匹配上請求的 server_name 值
    • https:如果開啟了 TLS/SSL 則返回 on,否則返回空
    • request_completion:若請求處理完則返回 OK,否則返回空
    • request_id:以 16 進制輸出的請求表示 id,該 id 共含有 16 個字節,是隨機生成的
    • request_filename:待訪問文件的完整路徑
    • document_root:由 URI 和 root、alias 規則生成的文件夾路徑
    • realpath_root:將 document_root 中的軟鏈接等換成真實路徑
    • limit_rate:返回客戶端響應時的速度上限,單位為每秒字節數。可以通過 set 指令修改對請求產生的效果

    發送 HTTP 響應時相關的變量

    • body_bytes_sent:響應中 body 包體的長度

    • bytes_sent:全部 http 響應的長度

    • status:http 響應中的返回碼

    • sent_trailer_名字:把響應結尾內容里的值返回

    • sent_http_頭部名字:響應中某個具體頭部的值

      特殊處理,下面這些變量需要經過特殊處理:

      • sent_http_content_type
      • sent_http_content_length
      • sent_http_location
      • sent_http_last_modified
      • sent_http_connection
      • sent_http_keep_alive
      • sent_http_transfer_encoding
      • sent_http_cache_control
      • sent_http_link

      通用:除了上面這些頭部,其他的頭部都是通用型的,也就是可以直接拿來用。

    Nginx 系統變量

    • time_local:以本地時間標準輸出的當前時間,例如 14/Nov/2018:15:55:37 +0800
    • time_iso8601:使用 ISO8601 標準輸出的當前時間,例如 2018-11-14T15:55:37+08:00
    • nginx_version:Nginx 版本號
    • pid:所屬 worker 進程的進程 id
    • pipe:使用了管道則返回 p,否則返回 .
    • hostname:所在服務器的主機名,與 hostname 命令輸出一致
    • msec:1970 年 1 月 1 日到現在的時間,單位為秒,小數點后精確到毫秒

    實戰

    配置文件:

    log_format  vartest  '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status bytes_sent=$bytes_sent body_bytes_sent=$body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$sent_http_abc"';
    
    server {
    	server_name var.ziyang.com localhost;
    	#error_log logs/myerror.log debug;
    	access_log logs/vartest.log vartest;
    	listen 9090;
    	
    	location / {
    		set $limit_rate 10k;
            # return 200; tcpinfo: $tcpinfo_rtt,$tcpinfo_rttvar, $tcpinfo_snd_cwnd, $tcpinfo_rcv_space 
    		return 200 '
    arg_a: $arg_a,arg_b: $arg_b,args: $args
    connection: $connection,connection_requests: $connection_requests
    cookie_a: $cookie_a
    uri: $uri,document_uri: $document_uri, request_uri: $request_uri
    request: $request
    request_id: $request_id
    server: $server_addr,$server_name,$server_port,$server_protocol
                
    host: $host,server_name: $server_name,http_host: $http_host
    limit_rate: $limit_rate
    hostname: $hostname
    content_length: $content_length
    status: $status
    body_bytes_sent: $body_bytes_sent,bytes_sent: $bytes_sent
    time: $request_time,$msec,$time_iso8601,$time_local
    ';
    	}	
    }
    

    從上面這個配置文件中,我們可以看出來,返回的響應裡面包含了一系列的變量,實際驗證一下:

      test_nginx curl -H 'Content-Length: 0' -H 'Cookie: a=c1' 'localhost:9090?a=1&b=22'
    
    arg_a: 1,arg_b: 22,args: a=1&b=22
    connection: 2,connection_requests: 1
    cookie_a: c1
    uri: /,document_uri: /, request_uri: /?a=1&b=22
    request: GET /?a=1&b=22 HTTP/1.1
    request_id: 5d40b1ff29d2b87d5db5c4f95ebf5e4d
    server: 127.0.0.1,var.ziyang.com,9090,HTTP/1.1
    host: localhost,server_name: var.ziyang.com,http_host: localhost:9090
    limit_rate: 10240
    hostname: yuanzizhen.local
    content_length: 0
    status: 200
    body_bytes_sent: 0,bytes_sent: 0
    time: 0.000,1590842354.866,2020-05-30T20:39:14+08:00,30/May/2020:20:39:14 +0800
    

    大家可以對比一下響應和配置文件中的值是不是一一對應的,更加深刻的理解一下變量的含義。

    好了,這一節咱們學習了。關於 Nginx 的變量就講完了,下一節講一下實際應用變量的兩個模塊,大家會有更深刻的理解。

    本文首發於我的個人博客:iziyang.github.io,所有配置文件我已經放在了 Nginx 配置文件,大家可以自取。

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

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

  • .Net Core微服務入門全紀錄(二)——Consul-服務註冊與發現(上)

    .Net Core微服務入門全紀錄(二)——Consul-服務註冊與發現(上)

    前言

    上一篇【.Net Core微服務入門全紀錄(一)——項目搭建】講到要做到服務的靈活伸縮,那麼需要有一種機制來實現它,這個機制就是服務註冊與發現。當然這也並不是必要的,如果你的服務實例很少,並且很穩定,那麼就沒有必要使用服務註冊與發現。

    服務註冊與發現

    • 服務註冊:簡單理解,就是有一個註冊中心,我們的每個服務實例啟動時,都去註冊中心註冊一下,告訴註冊中心我的地址,端口等信息。同樣的服務實例要刪除時,去註冊中心刪除一下,註冊中心負責維護這些服務實例的信息。
    • 服務發現:既然註冊中心維護了各個服務實例的信息,那麼客戶端通過註冊中心就很容易發現服務的變化了。

    有了服務註冊與發現,客戶端就不用再去配置各個服務實例的地址,改為從註冊中心統一獲取。
    那註冊中心又是怎麼保證每個地址的可用狀態呢,假如某個實例掛了怎麼辦呢?原則上掛掉的實例不應該被客戶端獲取到,所以就要提到:健康檢查 。

    • 健康檢查:每個服務都需要提供一個用於健康檢查的接口,該接口不具備業務功能。服務註冊時把這個接口的地址也告訴註冊中心,註冊中心會定時調用這個接口來檢測服務是否正常,如果不正常,則將它移除,這樣就保證了服務的可用性。

    常見註冊中心有 Consul、ZooKeeper、etcd、Eureka。

    Consul

    Consul官網:https://www.consul.io/
    Consul的主要功能有服務註冊與發現、健康檢查、K-V存儲、多數據中心等。

    • Consul安裝:很簡單,直接在官網下載解壓即可。
    • Consul運行:在consul.exe目錄下打開命令行執行 consul.exe agent -dev
    • 瀏覽器訪問:http://localhost:8500/

      Consul已成功運行。

    服務註冊

    • 首先Nuget安裝一下Consul:

      這個類庫里封裝了Consul的api操作,方便我們直接使用。當然自己去寫http調用Consul的接口也不是不行。。。接口說明:https://www.consul.io/api-docs

    • 改造一下訂單服務的代碼:

    ConsulHelper.cs:

        public static class ConsulHelper
        {
            /// <summary>
            /// 服務註冊到consul
            /// </summary>
            /// <param name="app"></param>
            /// <param name="lifetime"></param>
            public static IApplicationBuilder RegisterConsul(this IApplicationBuilder app, IConfiguration configuration, IHostApplicationLifetime lifetime) 
            {
                var consulClient = new ConsulClient(c =>
                {
                    //consul地址
                    c.Address = new Uri(configuration["ConsulSetting:ConsulAddress"]);
                });
    
                var registration = new AgentServiceRegistration()
                {
                    ID = Guid.NewGuid().ToString(),//服務實例唯一標識
                    Name = configuration["ConsulSetting:ServiceName"],//服務名
                    Address = configuration["ConsulSetting:ServiceIP"], //服務IP
                    Port = int.Parse(configuration["ConsulSetting:ServicePort"]),//服務端口 因為要運行多個實例,端口不能在appsettings.json里配置,在docker容器運行時傳入
                    Check = new AgentServiceCheck()
                    {
                        DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服務啟動多久后註冊
                        Interval = TimeSpan.FromSeconds(10),//健康檢查時間間隔
                        HTTP = $"http://{configuration["ConsulSetting:ServiceIP"]}:{configuration["ConsulSetting:ServicePort"]}{configuration["ConsulSetting:ServiceHealthCheck"]}",//健康檢查地址
                        Timeout = TimeSpan.FromSeconds(5)//超時時間
                    }
                };
    
                //服務註冊
                consulClient.Agent.ServiceRegister(registration).Wait();
    
                //應用程序終止時,取消註冊
                lifetime.ApplicationStopping.Register(() =>
                {
                    consulClient.Agent.ServiceDeregister(registration.ID).Wait();
                });
    
                return app;
            }
        }
    

    appsettings.json:

    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft": "Warning",
          "Microsoft.Hosting.Lifetime": "Information"
        }
      },
      "AllowedHosts": "*",
      "ConsulSetting": {
        "ServiceName": "OrderService",
        "ServiceIP": "localhost",
        "ServiceHealthCheck": "/healthcheck",
        "ConsulAddress": "http://host.docker.internal:8500"//注意,docker容器內部無法使用localhost訪問宿主機器,如果是控制台啟動的話就用localhost
      }
    }
    

    Startup.cs:

        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddControllers();
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
    
                app.UseRouting();
    
                app.UseAuthorization();
    
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllers();
                });
    
                //服務註冊
                app.RegisterConsul(Configuration, lifetime);
            }
        }
    

    OrdersController.cs:

        [Route("[controller]")]
        [ApiController]
        public class OrdersController : ControllerBase
        {
            private readonly ILogger<OrdersController> _logger;
            private readonly IConfiguration _configuration;
    
            public OrdersController(ILogger<OrdersController> logger, IConfiguration configuration)
            {
                _logger = logger;
                _configuration = configuration;
            }
    
            [HttpGet]
            public IActionResult Get()
            {
                string result = $"【訂單服務】{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}——" +
                    $"{Request.HttpContext.Connection.LocalIpAddress}:{_configuration["ConsulSetting:ServicePort"]}";
                return Ok(result);
            }
        }
    

    HealthCheckController.cs:

        [Route("[controller]")]
        [ApiController]
        public class HealthCheckController : ControllerBase
        {
            /// <summary>
            /// 健康檢查接口
            /// </summary>
            /// <returns></returns>
            [HttpGet]
            public IActionResult Get()
            {
                return Ok();
            }
        }
    

    至此就完成了服務註冊,取消註冊,健康檢查等功能的代碼編寫。

    • 同樣的改造一下產品服務,代碼差不多一樣,就不貼了。

    運行服務

    繼續在docker中運行服務實例,不習慣docker的話用控制台啟動也行。–ConsulSetting:ServicePort參數就是傳入容器的端口信息。

    docker build -t orderapi:1.0 -f ./Order.API/Dockerfile .
    docker run -d -p 9060:80 --name orderservice orderapi:1.0 --ConsulSetting:ServicePort="9060"
    docker run -d -p 9061:80 --name orderservice1 orderapi:1.0 --ConsulSetting:ServicePort="9061"
    docker run -d -p 9062:80 --name orderservice2 orderapi:1.0 --ConsulSetting:ServicePort="9062"
    
    docker build -t productapi:1.0 -f ./Product.API/Dockerfile .
    docker run -d -p 9050:80 --name productservice productapi:1.0 --ConsulSetting:ServicePort="9050"
    docker run -d -p 9051:80 --name productservice1 productapi:1.0 --ConsulSetting:ServicePort="9051"
    docker run -d -p 9052:80 --name productservice2 productapi:1.0 --ConsulSetting:ServicePort="9052"
    

    至此,6個服務器實例都已運行,並且成功註冊到Consul。

    隨便停止2個服務:

    可以看到停止的服務已經在Consul中被移除。注意,這個是我們停止程序時主動調用Consul移除的。

    //應用程序終止時,取消註冊
    lifetime.ApplicationStopping.Register(() =>
    {
        consulClient.Agent.ServiceDeregister(registration.ID).Wait();
    });
    

    當然程序發生異常,健康檢查不能正確響應的話,Consul也會移除,有一點區別。

    那麼註冊,發現,健康檢查功能都完成了,下一步就該考慮客戶端如何拿到這些服務實例的地址了。

    代碼放在:https://github.com/xiajingren/NetCoreMicroserviceDemo

    未完待續…

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

    【其他文章推薦】

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

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

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

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

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

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

  • Vue —— 精講 VueRouter(1)

    最近被Boos調去給新人做培訓去了,目前把自己整理的一些東西分享出來,希望對大家有所幫助

    demo源代碼地址:https://github.com/BM-laoli/BMlaoli-learn-VueRouter

    本章節為VueRouter前端 路由的章節部分

    大綱

    一、基本概念

    路由就是通過網絡把訊息從源地址傳輸到目的地的活動
    需要一些映射表

    1. 做路由
    2. 做信息的轉發(核心就是:轉發)

    後端路由還有前端路由,後端渲染和前端渲染

    前端渲染(前後端分離API生態),後端渲染(view嵌套一起)

    前端路由的核心概念
    地址變化的時候改變url的時候,不進行整體頁面刷新

    改變url但是不刷新頁面,的解決方式

    我們有這樣的一個需求,改變url跳轉地址,我們獲取新的頁面,但是不希望頁面發生刷新

    解決方案1:locaion.hash = ‘/’

    這個是vueRouter的底層實現

    監聽hash的變化,從而改變網頁數據的獲取機制,渲染對應的組件,

    解決方案2:H5的histroray模式

    1. pushState
      history.pushState({},”,’home’),第三個參數就是url

    這裏的push實際上就是一個棧結構(先進后出),

    假設我們這裏需要回去,使用back()彈棧

    history.pushState({},'','home'),
    history.pushState({},'','about'),
    history.pushState({},'','user'),
    
    //執行這個之後就能進行back()出棧了
    history.back(),
    // 此時的url是 /about
    
    
    1. repalceState

    這裡有一個方法和push方法很像,但是不會back()不能點擊後腿按鈕

    history.repalceState({},'','user'),
    
    1. go

    這裏的go是對棧的一個操作,
    go(-1)彈出一個
    go(-2)彈出二個

    go(1)壓入一個
    go(2)壓入二個

    go(-1)
    

    以上就是我們的基本的前端路由原理

    二、v-router基本使用

    前端三大框架都有自己的router,可以用來構建SPA應用

    使用小提示,還是非常非常的簡單的:

    1. 如果你沒有安裝就需要 npm install vue-router去安裝
      • 導入路由對象,並且調用Vue.use(VueRouter)安裝這個路由插件
      • 創建路由實例,傳入映射配置wxain
      • 在vue實例中掛載創建好了的路由

    1.導入路由對象,並且配置optionn給路由

    /router/index.js

    
    /**
     * 配置路由相關的信息
     */
    // 1. 導入
     import Router from 'vue-router'
     
     // 2.1 導入vue實例
    import Vue from 'vue'
    
    // 導入組件
    import Home from '../components/Home.vue'
    import About from '../components/About.vue'
    
    
    // 2.2使用路由(插件),安裝插件,vue的插件,都是這樣安裝,Vue.use
    Vue.use(Router)
    
    // 3. 創建路路由對象,這個就是在Router裏面配置映射和對象等東西
    
    // 4. 抽離配置項出來
    const routes = []
    
    const router = new Router({routes})
    
    //4. 導出
    export default router 
     
    
    

    2.配置路由映射

    /router/index.js

    const routes = [
     
     {path:'/home',component:Home},
     {path:'/about',component:About},
    
    ] 
    

    3.在實例中使用路由

    /main.js

    import Vue from 'vue'
    import App from './App'
    import router from './router'//注意啊模塊查找規則index.js
    
    Vue.config.productionTip = false
    
    /* eslint-disable no-new */
    new Vue({
      el: '#app',
      router,// 主要是在這裏掛載進去就好了
      render: h => h(App)
    }) 
    

    4.小心,我們的路由入口還有連接link

    /App.vue

    
    <template>
      <div id="app">
        <!-- //這兩個是一個全局祖冊過着個組件,這個就是一個a標籤 -->
        <router-link to='/home'>首頁</router-link>
        <router-link to='/about'>關於</router-link>
        <!-- 路由出口,既:渲染的出口,這個就是一個佔位符號 -->
        <router-view></router-view>
      </div>
    </template>
    
    

    以下是我們的兩個組件

    /Home.vue

    <template>
        <div>
            <h2>我是首頁</h2>
            <p>我是首頁內容哈哈哈</p>
        </div>
    </template>
    
    <script>
        export default {
            
        }
    </script>
    
    <style scoped>
    
    </style>
    

    /About.vue

    <template>
        <div>
            <h2>我是關於頁面</h2>
            <p>我是首關於內容哈哈哈</p>
        </div>
    </template>
    
    <script>
        export default {
            
        }
    </script>
    
    <style>
    
    </style>
    

    以上就是我們非常簡單的使用

    三、其它的知識點補充

    路由的默認值,並且修改成mode=>hisary模式

    我們希望默認显示的就是一個首頁
    解決方式,映射一個’/’,然後進行重定向
    /index.js

      {
        path:'/',
        redirect:'/home'
      },
    

    我們為什麼要去做這調整成一個history,因為我們希望去掉#這個標識

    只需要在new 的時候指定一下就好了
    /index,js

    const router = new Router({
      routes,
      mode:"history"//就是這裏的這個更改路由方式
    })
    

    router-link的屬性

    1. tage
      to是一個屬性 ,默認是渲染成一個a鏈接,假設我現在需要默認渲染成一個buttmm怎麼辦呢?
      加一個tag就好了
        <router-link to='/home' tag='button'  >首頁</router-link>
    
    1. 更改模式replceStats 不允許瀏覽器回退
      replace加上去就好了
    <router-link to='/about' tag='button' replace >關於</router-link>
    
    1. 我們可以利用一些默認的東西去非常方便的做到想要的效果
    <style>
    .router-link-active{
      color: blue;
    }
    </style>
    

    替換值:我們希望不要怎麼長,我們希望.active就能改樣式怎麼搞?
    加一個active-calss就好了,這個直接就是acitve做為類就好了

     <router-link to='/home' tag='button'  active-class  >首頁</router-link>
     <style>
        .active{
            bgc:red
        }
     </style>
    

    代碼路由跳轉,意思就是重定向

    注意啊!route != router
    在我們學習路由的時候,this.$router是一個非常重要的對象

    這個東西在開中經常的使用

    // this.$router.push('重定向的值就好了')。
    // this.$router.push('/home')
    // 如果你不想有出現回退按鈕,這樣來做就好了
    this.$router.replace('/home')
    

    四、動態路由參數

    這裏只是簡單的介紹了理由傳參的地址欄拼接模式,但是還有更多更奇奇怪怪的傳值方式,詳見官方Router文檔,

    this.$router.parmas
    // 這個parmas裏面就是我們的路由參數存放點
    

    這裏我們有這樣的一個需求,我們希望點擊user頁面的時候可以,得到任意的路由參數

    比如我們現在/user/zhnsang,的時候可以獲取zhangshang,/user/lishi的時候可以獲取lishi>

    1. 首先我們需要在路由裏面加:
      /router/index.js
       {
            path: "/user/:usermsg",
            component: User
        }
    ]
    
    1. 頁面傳遞數據
      /App.vue
    router-link :to="'/user/'+username">用戶相關</router-link>
    <!-- 路由出口,既:渲染的出口,這個就是一個佔位符號 -->
    <router-view></router-view>
      </div>
    </template>
    
    <script>
    export default {
      name: 'App',
      data() {
        return {
          username: 'lisi'
        }
      },
    
    
    1. 頁面獲取數據

    一定要注意了,一定是rouer裏面定義的才能從另一路由拿出來

    /User.vue

    
    <template>
        <div>
            <h2>我是用戶相關專業</h2>
            <p>我是用戶訊息相關頁面,嘿嘿嘿嘿嘿</p>
            <h1>{{ $route.params.usermsg }}44</h1>
            <hr>
            <h2>{{username}}</h2>
        </div>
    </template>
    
    <script>
        export default {    
            computed: {
                username() {
                    return this.$route.params.usermsg
                }
            },
        }
    </script>
    
    <style scpoe>
    
    </style>
    

    五、細節詳解

    注意啊!再說一遍route != router

    注意啊,這裏的$route實際上是我們在main裏面new的一個Router得到的,
    並且 這個route對象是隨着請求的地方不一樣,而改變的。也就是說,這個的route是當前頁面中的route對象,而且在vue只能只有一個route實例存在

    六、 Vue的webpack打包詳解 + 路由懶加載

    一個vue項目的簡單打包目錄結構分析

    我們來看看,在一個vue項目中,簡單的三個文件是怎麼打包的

    假設目前有這樣的三個文件 ,我們需要對他們進行打包,mian是入口,有一個add業務,有一個math依賴模塊。那麼我們webpack打包成的三個文件到底是如何運行的呢?

    在vue中 使用webpack打包的時候,會把一些東西給分模塊的打包出來,它打包的東西的目錄結構如下
    裏面我們實際打包的時候會把css,js都給分開,各自有各自的作用

    | dist
    | ---static
    | ---css
    | ---js
    | -----app.XXXX.js         (這個是項目的業務邏輯所在)
    | -----manifest.xxxx.js    (這個是底層打包的依賴文件所在)
    | -----vendor.xxxx.js      (這個是依賴所在)
    | idnex.html
    

    路由懶加載

    1. 概念的理解

    目前呢,我們打包的情況是這樣的:我們所有的代碼都是集中放在了以一個app.xxx.js文件中,這樣其實不利於後期的維護和開發,因為如果我們有很多很多的大量的代碼的時候,我們的這個文件就會變得非常非常的大,於是呢,我們就需要路由懶加載,所謂懶加載就是:‘在需要的時候,才去加載某個資源文件’,路由懶加載,就是把每一個路由對應的業務邏輯代碼,在打包的時候分割到不同的js文件中,如何在需要用的時候再去請求它

    經過這樣的打包的懶加載之後,我們的目錄會變成這個樣子

    | dist
    | ---static
    | ---css
    | ---js
    | -----0.xxx.js            (假設是路由home的業務邏輯代碼)
    | -----1.xxx.js             (假設是路由about的業務邏輯代碼)
    | -----app.XXXX.js         (這個是項目的業務邏輯所在)
    | -----manifest.xxxx.js    (這個是底層打包的依賴文件所在)
    | -----vendor.xxxx.js      (這個是依賴所在)
    | idnex.html
    
    1. 如何使用

    使用非常的簡單,主要有如下的三種方式去使用,但是我最喜歡的還是最後一種方式
    /rouetr/index.js

    - 使用vue的異步組價和webpack的寫法,早期的時候
    const Home = resolve =>{ require.ensure(['../compenet/Home.vue'],()=>{
       resolve (require('../compenet/Home.vue'))
    })}
    
    - AMD規範的寫法
    const About = resolve =>{ require(['../compenent/About.vue'],resolve) }
    
    
    - ES6的結合異步組件的方式(最常用)
    const Home = () => import('../compenet/Home.vue')
    

    實際的使用
    /router/index.js

    /**
     * 配置路由相關的信息
     */
    // 1. 導入
    import Router from 'vue-router'
    
    // 2.1 導入vue實例
    import Vue from 'vue'
    
    // 導入組件
    // import Home from '../components/Home.vue'
    // import About from '../components/About.vue'
    // import User from '../components/User'
    const Home = () =>
        import ('../components/Home.vue')
    const About = () =>
        import ('../components/About.vue')
    const User = () =>
        import ('../components/User')
    
    
    // 2.2使用路由(插件),安裝插件,vue的插件,都是這樣安裝,Vue.use
    Vue.use(Router)
    
    // 3. 創建路路由對象,這個就是在Router裏面配置映射和對象等東西
    
    // 4. 抽離配置項出來
    const routes = [{
            path: '/',
            redirect: '/home'
        },
        {
            path: '/home',
            component: Home
        },
        {
            path: '/about',
            component: About
        },
        {
            path: "/user/:usermsg",
            component: User
        }
    ]
    
    const router = new Router({
        routes,
        mode: "history"
    })
    
    //4. 導出
    export default router
    
    //6. 去main裏面掛載
    

    七、 路由嵌套

    我們目前有這樣的一個需求:我們希望我們在hone下,可以/home/new去到home下的一個子組件,/home/message去到另一個子組件

    1. 首先 我們需要有組件
      /components/HomeMessage.vue
    <template>
        <div>
          <ul>
              <li1>我是消息1</li1>
              <li2>我是消息2</li2>
              <li3>我是消息3</li3>
              <li4>我是消息4</li4>
          </ul>
        </div>
    </template>
    
    <script>
        export default {
            name:"HomeMessage"
        }   
    </script>
    
    <style>
    
    </style>
    
    

    /components/HomeNews

    <template>
        <div>
        <ul>
            <li1>新1</li1>
            <li2>新2</li2>
            <li3>新3</li3>
            <li4>新4</li4>
            <li5>新5</li5>
        </ul>
        </div>
    </template>
    
    <script>
        export default {
            name:"HomeNews"
        }
    </script>
    
    <style>
    
    </style>
    
    1. 在路由裏面去配置
    const HomeNews = () =>
        import ('../components/HomeNews')
    const HomeMessage = () =>
        import ('../components/HomeNews')
    
    
    // 2.2使用路由(插件),安裝插件,vue的插件,都是這樣安裝,Vue.use
    Vue.use(Router)
    
    // 3. 創建路路由對象,這個就是在Router裏面配置映射和對象等東西
    
    // 4. 抽離配置項出來
    const routes = [{
            path: '/',
            redirect: '/home'
        },
        {
            path: '/home',
            component: Home,
            children: [{
                    path: '',
                    redirect: 'news'
                },
                {
                    path: 'news',// 這裏寫路由實際上應該是/home/news,這裏只是一個相對路由地址,
                    component: HomeNews
                },
                {
                    path: 'message',
                    component: HomeMessage
                },
    
            ]
        },
        {
    
    1. 打入口router-view(瞎起的名字實際上就是路由的佔位符)
      /Home.vue
    <template>
        <div>
            <h2>我是首頁</h2>
            <p>我是首頁內容哈哈哈</p>
         <router-link to="/home/news">news</router-link>
         <router-link to="/home/message">message</router-link>
        <router-view></router-view>
        </div>
    </template>
    
    <script>
        export default {
            
        }
    </script>
    
    <style scoped>
    
    </style>
    

    這裏如果是有關狀態的保持,我們需要使用key-alive,後面我們再做詳細的講解

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

    【其他文章推薦】

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

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

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

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

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

  • 小師妹學JavaIO之:用Selector來發好人卡

    小師妹學JavaIO之:用Selector來發好人卡

    目錄

    • 簡介
    • Selector介紹
    • 創建Selector
    • 註冊Selector到Channel中
    • SelectionKey
    • selector 和 SelectionKey
    • 總的例子
    • 總結

    簡介

    NIO有三寶:Buffer,Channel,Selector少不了。本文將會介紹NIO三件套中的最後一套Selector,並在理解Selector的基礎上,協助小師妹發一張好人卡。我們開始吧。

    Selector介紹

    小師妹:F師兄,最近我的桃花有點旺,好幾個師兄莫名其妙的跟我打招呼,可是我一心向著工作,不想談論這些事情。畢竟先有事業才有家嘛。我又不好直接拒絕,有沒有什麼比較隱晦的方法來讓他們放棄這個想法?

    更多內容請訪問www.flydean.com

    這個問題,我沉思了大約0.001秒,於是給出了答案:給他們發張好人卡吧,應該就不會再來糾纏你了。

    小師妹:F師兄,如果給他們發完好人卡還沒有用呢?

    那就只能切斷跟他們的聯繫了,來個一刀兩斷。哈哈。

    這樣吧,小師妹你最近不是在學NIO嗎?剛好我們可以用Selector來模擬一下發好人卡的過程。

    假如你的志偉師兄和子丹師兄想跟你建立聯繫,每個人都想跟你建立一個溝通通道,那麼你就需要創建兩個channel。

    兩個channel其實還好,如果有多個人都想同時跟你建立聯繫通道,那麼要維持這些通道就需要保持連接,從而浪費了資源。

    但是建立的這些連接並不是時時刻刻都有消息在傳輸,所以其實大多數時間這些建立聯繫的通道其實是浪費的。

    如果使用Selector就可以只啟用一個線程來監聽通道的消息變動,這就是Selector。

    從上面的圖可以看出,Selector監聽三個不同的channel,然後交給一個processor來處理,從而節約了資源。

    創建Selector

    先看下selector的定義:

    public abstract class Selector implements Closeable
    

    Selector是一個abstract類,並且實現了Closeable,表示Selector是可以被關閉的。

    雖然Selector是一個abstract類,但是可以通過open來簡單的創建:

    Selector selector = Selector.open();
    

    如果細看open的實現可以發現一個很有趣的現象:

    public static Selector open() throws IOException {
            return SelectorProvider.provider().openSelector();
        }
    

    open方法調用的是SelectorProvider中的openSelector方法。

    再看下provider的實現:

     public SelectorProvider run() {
       if (loadProviderFromProperty())
            return provider;
        if (loadProviderAsService())
            return provider;
          provider = sun.nio.ch.DefaultSelectorProvider.create();
          return provider;
        }
     });
    

    有三種情況可以加載一個SelectorProvider,如果系統屬性指定了java.nio.channels.spi.SelectorProvider,那麼從指定的屬性加載。

    如果沒有直接指定屬性,則從ServiceLoader來加載。

    最後如果都找不到的情況下,使用默認的DefaultSelectorProvider。

    關於ServiceLoader的用法,我們後面會有專門的文章來講述。這裏先不做多的解釋。

    註冊Selector到Channel中

    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress("localhost", 9527));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    

    如果是在服務器端,我們需要先創建一個ServerSocketChannel,綁定Server的地址和端口,然後將Blocking設置為false。因為我們使用了Selector,它實際上是一個非阻塞的IO。

    注意FileChannels是不能使用Selector的,因為它是一個阻塞型IO。

    小師妹:F師兄,為啥FileChannel是阻塞型的呀?做成非阻塞型的不是更快?

    小師妹,我們使用FileChannel的目的是什麼?就是為了讀文件呀,讀取文件肯定是一直讀一直讀,沒有可能讀一會這個channel再讀另外一個channel吧,因為對於每個channel自己來講,在文件沒讀取完之前,都是繁忙狀態,沒有必要在channel中切換。

    最後我們將創建好的Selector註冊到channel中去。

    SelectionKey

    SelectionKey表示的是我們希望監聽到的事件。

    總的來說,有4種Event:

    • SelectionKey.OP_READ 表示服務器準備好,可以從channel中讀取數據。
    • SelectionKey.OP_WRITE 表示服務器準備好,可以向channel中寫入數據。
    • SelectionKey.OP_CONNECT 表示客戶端嘗試去連接服務端
    • SelectionKey.OP_ACCEPT 表示服務器accept一個客戶端的請求
    public static final int OP_READ = 1 << 0;
    public static final int OP_WRITE = 1 << 2;
    public static final int OP_CONNECT = 1 << 3;
    public static final int OP_ACCEPT = 1 << 4;
    

    我們可以看到上面的4個Event是用位運算來定義的,如果將這個四個event使用或運算合併起來,就得到了SelectionKey中的interestOps。

    和interestOps類似,SelectionKey還有一個readyOps。

    一個表示感興趣的操作,一個表示ready的操作。

    最後,SelectionKey在註冊的時候,還可以attach一個Object,比如我們可以在這個對象中保存這個channel的id:

    SelectionKey key = channel.register(
      selector, SelectionKey.OP_ACCEPT, object);
    key.attach(Object);
    Object object = key.attachment();
    

    object可以在register的時候傳入,也可以調用attach方法。

    最後,我們可以通過key的attachment方法,獲得該對象。

    selector 和 SelectionKey

    我們通過selector.select()這個一個blocking操作,來獲取一個ready的channel。

    然後我們通過調用selector.selectedKeys()來獲取到SelectionKey對象。

    在SelectionKey對象中,我們通過判斷ready的event來處理相應的消息。

    總的例子

    接下來,我們把之前將的串聯起來,先建立一個小師妹的ChatServer:

    public class ChatServer {
    
        private static String BYE_BYE="再見";
    
        public static void main(String[] args) throws IOException, InterruptedException {
            Selector selector = Selector.open();
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress("localhost", 9527));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            ByteBuffer byteBuffer = ByteBuffer.allocate(512);
    
            while (true) {
                selector.select();
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> iter = selectedKeys.iterator();
                while (iter.hasNext()) {
                    SelectionKey selectionKey = iter.next();
                    if (selectionKey.isAcceptable()) {
                        register(selector, serverSocketChannel);
                    }
                    if (selectionKey.isReadable()) {
                        serverResonse(byteBuffer, selectionKey);
                    }
                    iter.remove();
                }
                Thread.sleep(1000);
            }
        }
    
        private static void serverResonse(ByteBuffer byteBuffer, SelectionKey selectionKey)
                throws IOException {
            SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
            socketChannel.read(byteBuffer);
            byteBuffer.flip();
            byte[] bytes= new byte[byteBuffer.limit()];
            byteBuffer.get(bytes);
            log.info(new String(bytes).trim());
            if(new String(bytes).trim().equals(BYE_BYE)){
                log.info("說再見不如不見!");
                socketChannel.write(ByteBuffer.wrap("再見".getBytes()));
                socketChannel.close();
            }else {
                socketChannel.write(ByteBuffer.wrap("你是個好人".getBytes()));
            }
            byteBuffer.clear();
        }
    
        private static void register(Selector selector, ServerSocketChannel serverSocketChannel)
                throws IOException {
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
        }
    }
    

    上面例子有兩點需要注意,我們在循環遍歷中,當selectionKey.isAcceptable時,表示服務器收到了一個新的客戶端連接,這個時候我們需要調用register方法,再註冊一個OP_READ事件到這個新的SocketChannel中,然後繼續遍歷。

    第二,我們定義了一個stop word,當收到這個stop word的時候,會直接關閉這個client channel。

    再看看客戶端的代碼:

    public class ChatClient {
    
        private static SocketChannel socketChannel;
        private static ByteBuffer byteBuffer;
    
        public static void main(String[] args) throws IOException {
    
            ChatClient chatClient = new ChatClient();
            String response = chatClient.sendMessage("hello 小師妹!");
            log.info("response is {}", response);
            response = chatClient.sendMessage("能不能?");
            log.info("response is {}", response);
            chatClient.stop();
    
        }
    
        public void stop() throws IOException {
            socketChannel.close();
            byteBuffer = null;
        }
    
        public ChatClient() throws IOException {
            socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9527));
            byteBuffer = ByteBuffer.allocate(512);
        }
    
        public String sendMessage(String msg) throws IOException {
            byteBuffer = ByteBuffer.wrap(msg.getBytes());
            String response = null;
            socketChannel.write(byteBuffer);
            byteBuffer.clear();
            socketChannel.read(byteBuffer);
            byteBuffer.flip();
            byte[] bytes= new byte[byteBuffer.limit()];
            byteBuffer.get(bytes);
            response =new String(bytes).trim();
            byteBuffer.clear();
            return response;
    
        }
    }
    

    客戶端代碼沒什麼特別的,需要注意的是Buffer的讀取。

    最後輸出結果:

    server收到: INFO com.flydean.ChatServer - hello 小師妹!
    client收到: INFO com.flydean.ChatClient - response is 你是個好人
    server收到: INFO com.flydean.ChatServer - 能不能?
    client收到: INFO com.flydean.ChatClient - response is 再見
    

    解釋一下整個流程:志偉跟小師妹建立了一個連接,志偉向小師妹打了一個招呼,小師妹給志偉發了一張好人卡。志偉不死心,想繼續糾纏,小師妹回復再見,然後自己關閉了通道。

    總結

    本文介紹了Selector和channel在發好人卡的過程中的作用。

    • 區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新
    • Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
    • Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
    • java程序員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程

    本文作者:flydean程序那些事

    本文鏈接:http://www.flydean.com/java-io-nio-selector/

    本文來源:flydean的博客

    歡迎關注我的公眾號:程序那些事,更多精彩等着您!

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

    【其他文章推薦】

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

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

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

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

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