分類: 3C資訊

  • 這款美系SUV底盤質感和操控都不錯 而且只賣9萬出頭!

    這款美系SUV底盤質感和操控都不錯 而且只賣9萬出頭!

    底盤有高級感創酷的后懸架用的是複合扭力梁式非獨立懸架,調校出來很有厚實感。在濾震的時,總能很巧妙地化解,傳進車廂內的不過是一股很柔和的力。不過,礙於車身偏高,在過急彎時的側傾有點大。乘坐表現中規中矩創酷的後排座椅靠背較直,且包裹性一般。

    說起美系SUV,不得不提昂科拉與創酷。雖然它們都沒有美國車的特性,但卻是極具中國特色的美系車。下面編者我就講講定位稍低一點的創酷。

    上汽通用雪佛蘭-創酷

    指導價:9.99-14.99萬

    外觀時尚精緻

    創酷的車身尺寸為4255*1776*1675mm,軸距為2555mm。前臉設計就像瘦身成功的胖子,展現出一些美系的肌肉感。鍍鉻裝飾條的使用不多,但圍着中網和霧燈區的設計,顯得恰到好處。

    側面很簡潔,只是在門板下方到后翼子板處隱約露出了一條“肌肉線”。

    后擋風玻璃的面積偏小,三段式的尾燈設計很有層次感,隱藏式的排氣更顯高級。

    內飾設計簡約為主

    內飾以棕色和黑色為主色調,中控檯面板上下方的波浪形設計很時尚。烤黑鋼琴漆的显示屏面板凸顯高級,銀色飾條將其與中央出風口圍起來,很有整體性。空調旋鈕區簡潔明了,同樣的設計還有方向盤。方向盤僅右側有少量按鈕,看上去讓人很舒服。

    動力輸出輕快

    創酷只有1.4T發動機可選,與之匹配的是6擋手動和6擋手自一體變速箱。最大輸出143馬力和205牛米,最大扭矩在1800轉開始爆發,能維持到4800轉,持續時間足夠長。

    在市區中遊走,車子感覺很輕快,不會有乏力的情況。加速時,也能很好地執行動作。這副6AT在急加速時,能感覺到一絲絲闖動。這種感覺很輕微,乘客是感覺不到的,只有細心的駕駛者才會發現。

    底盤有高級感

    創酷的后懸架用的是複合扭力梁式非獨立懸架,調校出來很有厚實感。在濾震的時,總能很巧妙地化解,傳進車廂內的不過是一股很柔和的力。不過,礙於車身偏高,在過急彎時的側傾有點大。

    乘坐表現中規中矩

    創酷的後排座椅靠背較直,且包裹性一般。不過座墊的長度對於中等身材的人來說很充足,且腿部和頭部都能有很好的空間。中央地板有一定凸起,對中間乘客的舒適型有不少影響。

    油耗表現與保養費用

    多位車主反映手動擋創酷的百公里綜合油耗為7.1L,而自動擋為8.6L。

    保養費用方面,無論是手動擋還是自動擋,六萬公里的總保養費用均為9355元。這是因為自動擋的變速箱油要到8萬公里才更換。這個費用相比同級其他對手來說,是偏高了一點。正常來說,應該在7000元左右。

    配置分析

    對於創酷這款車,自動擋車型來說,我會推薦買指導價為13.39萬的自動兩驅豪華型。儘管其比自動擋的最低配車型貴了14000元,但是多出了不少實用且增添高級感的配置,具體可看下錶。

    至於手動擋車型,我建議直接買指導價9.99萬的最低配車型。因為手動擋只有兩個車型可選,更高一級的車型要多花1萬元且是2016款,配置上也沒多出很多。

    編者總結

    創酷這台車其實內在與昂科拉是基本一致的,但是卻比昂科拉賣得要更便宜。在机械質感與底盤舒適性上都表現得很不錯,僅僅是保養費用和空間上會稍遜同級對手一點。如果你就想要一台美系SUV,創酷會是一個極具性價比的選擇。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

  • 10萬全新高顏值家轎即將上市 力爭成為國民首選?

    10萬全新高顏值家轎即將上市 力爭成為國民首選?

    整體處於平均水平,畢竟這類型的入門車型,性能不是最重要的,重要的是質量可靠以及保養維修便宜。競爭對手:福特福睿斯指導價:9。68-11。98萬福特福睿斯是這次現代悅動的頭號競爭對手,福睿斯使用的是福克斯平台,雖然換上了后扭力梁式懸架,但是比起福克斯更為舒適而且後排空間更大,深受消費者的喜好。

    最近公布的2016年全年榜單中,福睿斯以及英朗的優秀銷量告訴了我們“中國特供車”中國消費者是會多喜愛。看到這個現象,有着越來越多的車企進入這個市場。而一向以高顏值著稱的北京現代,就向我們透露這樣的信息,1月18號全新悅動下線,三月中下旬就可以買到一款有着超高顏值的全新悅動了。那麼這究竟是款怎樣的車型呢?

    全新悅動是北京現代攻佔入門級緊湊型轎車市場的武器,有着韓系車一貫的高顏值特性。犀利的開眼角大燈設計、大面積的六邊形前進氣格柵以及較低的視覺重心設計,讓它有着一種“現代領動”孿生兄弟的感覺,不過線條處理比起領動更為柔和,受眾年齡更為廣泛。

    單單看着側面就知道它的定位會比起領動要低,車身長度以及軸距預測將和福睿斯、科沃茲等入門緊湊型轎車處於同一水平。車頂線條流暢柔和,這樣可以在頭部空間能有着較大的保障。多幅式鋁合金輪轂和整車氣質非常相符,非常協調般配。

    在尾部造型上,全新悅動更像是自家弟弟——現代悅納,雖然採用的是家族史設計,但是比起領動以及悅納,全新悅動尾部造型更為圓潤。不過一樣使用小巧的鴨尾式設計,為車尾增添了一點亮色。

    (上圖為悅納1.6L自然吸氣發動機)

    動力方面全新悅動將搭載和悅納上的1.6L自然吸氣發動機,最大功率123馬力,變速箱方面將有着6速手動或者6速手自一體變速箱可以選擇。整體處於平均水平,畢竟這類型的入門車型,性能不是最重要的,重要的是質量可靠以及保養維修便宜。

    競爭對手:

    福特福睿斯

    指導價:9.68-11.98萬

    福特福睿斯是這次現代悅動的頭號競爭對手,福睿斯使用的是福克斯平台,雖然換上了后扭力梁式懸架,但是比起福克斯更為舒適而且後排空間更大,深受消費者的喜好。福睿斯相對於全新悅動來說,外觀設計面向的群眾年齡層會更高,家庭用戶佔了絕大多數。

    雪佛蘭科沃茲

    官方指導價:7.99-10.99萬

    和着福睿斯一樣,科沃茲也是一款“中國特供車”,推出市場時間較短的它還需要時間證明自己的實力,所以在今年它有可能會是一匹“黑馬”。而且在價格上,科沃茲還是非常親民,官方指導價還不到8萬,和競爭對手相比有着價格優勢,而在空間表現以及性能上也是這個級別的平均水準。

    編者總結:

    對於入門緊湊型車型,現代集團之前是處於放任自流的狀態,畢竟有着瑞納這款小型車就可以撐起這個價位的市場。但是在意識到入門緊湊型轎車的熱銷以後,推出一款全新的入門就直觀至關重要。

    而全新悅動相對於競爭對手而言就有着高顏值的特性,在筆者看來這會成為它的一大利器,畢竟能吸收更為年輕以及購買力更強的消費者。其次是價格,根據定位全新悅動價格極有可能在8-11萬之間。對於我們消費者來說,這樣一款高顏值、價格親民的入門緊湊型轎車是我們需要的。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

    ※超省錢租車方案

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

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

  • 油耗高達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地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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