分類
發燒車訊

別再寫一摞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)

目錄

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

分類
發燒車訊

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、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來發好人卡

目錄

  • 簡介
  • 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的博客

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

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

【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

手把手教你學Numpy,這些api不容錯過

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天是Numpy專題的第5篇文章,我們來繼續學習Numpy當中一些常用的數學和統計函數。

基本統計方法

在日常的工作當中,我們經常需要通過一系列值來了解特徵的分佈情況。比較常用的有均值、方差、標準差、百分位數等等。前面幾個都比較好理解,簡單介紹一下這個百分位數,它是指將元素從小到大排列之後,排在第x%位上的值。我們一般常用的是25%,50%和75%這三個值,通過這幾個值,我們很容易對於整個特徵的分佈有一個大概的了解。

前面三個指標:均值、方差、標準差都很好理解,我們直接看代碼就行。

median和percentile分別是求中位數與百分位數,它們不是Numpy當中array的函數,而是numpy的庫函數。所以我們需要把array當做參數傳入。percentile這個函數還需要額外傳入一個int,表示我們想要得到的百分位數,比如我們想要知道50%位置上的數,則輸入50。

除了這些之外,我們還會經常用到sum,min,max,argmin,argmax這幾個函數。sum,min,max很好理解,argmin和argmax的意思是獲取最小值和最大值的索引

這裏返回的索引有點奇怪,和我們想的不同,居然不是一個二維的索引而是一維的。實際上numpy的內部會將高維數組轉化成一維之後再進行這個操作,我們可以reshape一下數組來進行驗證:

這些只是api的基本用法,numpy當中支持的功能不僅如此。我們觀察一下這些函數會發現,它們的作用域都是一組數據,返回的是一組數據通過某種運算得到的結果。舉個例子,比如sum,是對一組數據的價格。std計算的是一組數據的標準差,這樣的函數我們稱為聚合函數

numpy當中的聚合函數在使用的時候允許傳入軸這個參數,限制它聚合的範圍。我們通過axis這個參數來控制,axis=0表示對列聚合,axis=1表示對行聚合。我們死記的話總是會搞混淆,實際上axis傳入的也是一個索引,表示第幾個索引的索引。我們的二維數組的shape是[行, 列],其中的第0位是行,第1位是列,可以認為axis是這個索引向量的一個索引。

我們可以來驗證一下:

可以看到axis=0和axis=1返回的向量的長度是不同的,因為以列為單位聚合只有4列,所以得到的是一個1 x 4的結果。而以行為單位聚合有5行,所以是一個1 x 5的向量。

除了上面介紹的這些函數之外,還有cumsum和cumprod這兩個api。其中cumsum是用來對數組進行累加運算,而cumprod是進行的累乘運算。只是在實際工作當中,很少用到,我就不展開細講了,感興趣的同學可以查閱api文檔了解一下。

bool數組的方法

我們之前在Python的入門文章當中曾經提到過,在Python中True和False完全等價於1和0。那麼在上面這些計算的方法當中,如果存在bool類型的值,都會被轉化成1和0進行的計算。

我們靈活運用這點會非常方便,舉個例子,假設我們要統計一批數據當中有多少條大於0。我們利用sum會非常方便:

bool數組除了可以應用上面這些基本的運算api之外,還有專門的兩個api,也非常方便。一個叫做any,一個叫做all。any的意思是只要數組當中有一個是True,那麼結果就是True。可以認為是Is there any True in the array的意思,同樣,all就是說只有數組當中都是True,結果才是True。對應的英文自然是Are the values in the array all True。

這個只要理解了,基本上很難忘記。

排序

Python原生的數組可以排序,numpy當中的數組自然也不例外。我們只需要調用sort方法就可以排序了,不過有一點需要注意,numpy中的sort默認是一個inplace的方法。也就是說我們調用完了sort之後,原數組的值就自動變化了。

如果寫成了arr = arr.sort()會得到一個None,千萬要注意。

同樣,我們也可以通過傳入軸這個參數來控制它的排序範圍,可以做到對每一列排序或者是對每一行排序,我們來看個例子:

這個是對列排序,如果傳入0則是對行排序,這個應該不難理解。

集合api

numpy當中還提供了一些面向集合的api,相比於針對各種計算的api,這些方法用到的情況比較少。常用的一般只有unique和in1d

unique顧名思義就是去重的api,可以返回一維array去重且排序之後的結果。我們來看個例子:

它等價於:

set(sorted(arr))

in1d是用來判斷集合內的元素是否在另外一個集合當中,函數會返回一個bool型的數組。我們也可以來看個例子:

除了這兩個api之外,還有像是計算並集並排序的union1d,計算差集的setdiff1d,計算兩個集合交集並排序的intersect1d等等。這些api的使用頻率實在是不高,所以就不贅述了。用到的時候再去查閱即可。

總結

今天我們聊了numpy當中很多常用的計算api,這些api在我們日常做機器學習和數據分析的時候經常用到。比如分析特徵分佈的時候,如果數據量很大是不適合作圖或者是可視化觀察的。這個時候可以從中位數、均值、方差和幾個關鍵百分位點入手,再比如在我們使用softmax多分類的時候,也會用到argmax來獲取分類的結果。

總之,今天的內容非常關鍵,在numpy整體的應用當中佔比很高,希望大家都能熟悉它們的基本用法。這樣即使以後忘記,用到的時候再查閱也還來得及。

今天的文章就是這些,如果喜歡本文,可以的話請點個關注,給我一點鼓勵,也方便獲取更多文章。

本文使用 mdnice 排版

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

【其他文章推薦】

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

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

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

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

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

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

分類
發燒車訊

無異常日誌,就不能排查問題了???

小聲逼逼

眾所周知,日誌是排查問題的重要手段。關於日誌設計,以及怎麼根據從【用戶報障】環節開始到秒級定位問題這個我們下一期說(絕非套路),這一期,主要講一下,在沒有異常日誌的情況下,如何定位問題。沒有日誌當真能排查問題,不會是標題黨吧!

案例一

從最大的同性交友網站中拉取【dubbo-spring-boot-project】的代碼。

然後把demo跑起來。

本場景是由真實案例改編,因為公司代碼比較複雜也不方便透露,而這個demo在github上大家都能找到,既保證了原汁原味,又能讓大家方便自己體驗排查過程。

好了,我們先設置owner = "feichao",然後看一下控制台

一切正常

那麼,當我設置成owner = "feichaozhenshuai!",再啟動

看似一切都正常,那麼,我們到控制台一看。

什麼情況,怎麼就沒owner了?

這是在哪個環節出問題了?其實肥朝當初在公司遇到這個問題的時候,場景比這個複雜得多。因為公司的業務里沒有owner的話,在運行時會出現一些其他異常,涉及公司業務這裏就不展開了,我們言歸正傳,為毛我設置成feichaozhenshuai!就不行了,那我設置成肥朝大帥比電腦會不會爆炸啊???

常見的錯誤做法是,把這個問題截圖往群里一丟,問“你們有沒有遇到過dubbo裏面,owner設置不生效的問題?”

而關注了肥朝公眾號的【真愛粉絲】會這麼問,“dubbo裏面設置owner卻不生效,你們覺得我要從個角度排查問題?”。一看到這麼正確的提問方式,我覺得我不回復你都不好意思。好了,回到主題,這個時候,沒有一點點錯誤日誌,但是卻設置不成功,我們有哪些排查手段?

套路一

直接找set方法,看看是不是代碼做了判斷,防止在owner字段裏面set肥朝真帥這種詞語,避免把帥這件事走漏風聲!。這麼一分析似乎挺有道理對吧,那麼,如何快速找到這個set方法呢?如圖

public void setOwner(String owner) {
    checkMultiName("owner", owner);
    this.owner = owner;
}

我們跟進checkMultiName代碼后發現

protected static void checkProperty(String property, String value, int maxlength, Pattern pattern) {
    if (StringUtils.isEmpty(value)) {
        return;
    }
    if (value.length() > maxlength) {
        throw new IllegalStateException("Invalid " + property + "=\"" + value + "\" is longer than " + maxlength);
    }
    if (pattern != null) {
        Matcher matcher = pattern.matcher(value);
        if (!matcher.matches()) {
            throw new IllegalStateException("Invalid " + property + "=\"" + value + "\" contains illegal " +
                    "character, only digit, letter, '-', '_' or '.' is legal.");
        }
    }
}

從異常描述就很明顯可以看出,原來owner裏面是只支持-_等這類特殊符號,!是不支持的,所以設置成不成功,和肥朝帥不帥是沒關係的,和後面的!是有關係的。擦,原來是肥朝想多了,給自己加戲了!!!

當然肥朝可以告訴你,在後面的版本,修復了這個bug,日誌會看得到異常了。這個時候你覺得問題就解決了?

我相信此時很多假粉就會關掉文章,或者說下次肥朝發了一些他們不喜歡看的文章(你懂的)后,他們就從此取關,但是肥朝想說,且慢動手!!!

你想嘛,萬一你以後又遇到類似的問題呢?而且源碼層次很深,就不是簡單的搜個set方法這麼簡單,這次給你搜到了set方法並解決問題,簡直是偶然成功。因此,我才多次強調,要持續關注肥朝,掌握更多套路。這難道是想騙你關注?我這分明是愛你啊!

那麼,萬一以後遇到一些吞掉異常,亦或者某些原因導致日誌沒打印,我們到底如何排查?

套路二

我們知道idea裏面有很多好用的功能,比如肥朝之前的【看源碼,我為何推薦idea?】中就提到了條件斷點,除此之外,還有一個被大家低估的功能,叫做異常斷點

肥朝掃了一眼,裏面的單詞都是小學的英語單詞,因此怎麼使用就不做過多解釋。遇到這個問題時,我們可以這樣設置異常斷點。

運行起來如下:

這樣,運行起來的時候,就會迅速定位到異常位置。然後一頓分析,應該很容易找出問題。

是不是有點感覺了?那我們再來一個題型練習一下。

案例二

我們先在看之前肥朝粉絲群的提問

考慮到部分粉絲不在群里,我就簡單描述一下這個粉絲的問題,他代碼有個異常,然後catch打異常日誌,但是日誌卻沒輸出。

當然你還是不理解也沒關係,我根據該粉絲的問題,給你搭建了一個最簡模型的demo,模型雖然簡單,但是問題是同樣的,原汁原味,熟悉的配方,熟悉的味道。git地址如下:【https://gitee.com/HelloToby/springboot-run-exception】

我們運行起來看一下

@Slf4j
public class HelloSpringApplicationRunListener implements SpringApplicationRunListener {

    public HelloSpringApplicationRunListener(SpringApplication application, String[] args) {
    }

    @Override
    public void starting() {

    }

    @Override
    public void environmentPrepared(ConfigurableEnvironment environment) {

    }

    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        throw new RuntimeException("歡迎關注微信公眾號【肥朝】");
    }

    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {

    }

    @Override
    public void finished(ConfigurableApplicationContext context, Throwable exception) {
    }
}

你會發現,一運行起來進程就停止,一點日誌都沒。絕大部分假粉絲遇到這個情況,都是菊花一緊,一點頭緒都沒,又去群里問”你們有沒有遇到過,Springboot一起來進程就沒了,但是沒有日誌的問題?“。正確提問姿勢肥朝已經強調過,這裏不多說。那麼我們用前面學到的排查套路,再來走一波

我們根據異常棧順藤摸瓜

我們從代碼中看出兩個關鍵單詞【reportFailure】、【context.close()】,經過斷點我們發現,確實是會先打印日誌,再關掉容器。但是為啥日誌先執行,再關掉容器,日誌沒輸出,容器就關掉了呢?因為,這個demo中,日誌是全異步日誌,異步日誌還沒執行,容器就關了,導致了日誌沒有輸出。

該粉絲遇到的問題是類似的,他是單元測試中,代碼中的異步日誌還沒輸出,單元測試執行完進程就停止了。知道了原理解決起來也很簡單,比如最簡單的,跑單元測試的時候末尾先sleep一下等日誌輸出。

在使用Springboot中,其實經常會遇到這種,啟動期間出現異常,但是日誌是異步的,日誌還沒輸出就容器停止,導致沒有異常日誌。知道了原理之後,要徹底解決這類問題,可以增加一個SpringApplicationRunListener

/**
 * 負責應用啟動時的異常輸出
 */
@Slf4j
public class OutstandingExceptionReporter implements SpringApplicationRunListener {

    public OutstandingExceptionReporter(SpringApplication application, String[] args) {
    }

    @Override
    public void starting() {

    }

    @Override
    public void environmentPrepared(ConfigurableEnvironment environment) {

    }

    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {

    }

    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {

    }

    @Override
    public void finished(ConfigurableApplicationContext context, Throwable exception) {
        if (exception != null) {
            log.error("application started failed",exception);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                log.error("application started failed", e);
            }
        }
    }
}

再啰嗦一句,其實日誌輸出不了,除了這個異步日誌的案例外,還有很多情況的,比如日誌衝突之類的,排查套路還很多,因此,建議持續關注,每一個套路,都想和你分享!

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

分類
發燒車訊

一文入門Kafka,必知必會的概念通通搞定

Kakfa在大數據消息引擎領域,絕對是沒有爭議的國民老公。

這是kafka系列的第一篇文章。預計共出20篇系列文章,全部原創,從0到1,跟你一起死磕kafka。

本文盤點了 Kafka 的各種術語並且進行解讀,術語可能比較枯燥,但真的是精髓中的精髓!

了解Kafka之前我們必須先掌握它的相關概念和術語,這對於後面深入學習 Kafka 各種功能將大有裨益。所以,枯燥你也得給我看完!

大概是有這麼些東西要掌握,不多不多,預計20分鐘可以吃透:

主題層

主題層有三個兒子,分別叫做:Topic、Partition、Replica。既然我說是三個兒子,那你懂了,是不可分割的整體。

Topic(主題)

Kafka 是分佈式的消息引擎系統,它的主要功能是提供一套完備的消息(Message)發布與訂閱解決方案。

在 Kafka 中,發布訂閱的對象是主題(Topic),你可以為每個業務、每個應用甚至是每類數據都創建專屬的主題。

一個Topic是對一組消息的歸納。也可以理解成傳統數據庫里的表,或者文件系統里的一個目錄。

Partition(分區)

一個Topic通常都是由多個partition組成的,創建topic時候可以指定partition數量。

分區優勢

為什麼需要將Topic分區呢?如果你了解其他分佈式系統,你可能聽說過分片、分區域等說法,比如 MongoDB 和 Elasticsearch 中的 Sharding、HBase 中的 Region,其實它們都是相同的原理。

試想,如果一個Topic積累了太多的數據以至於單台 Broker 機器都無法容納了,此時應該怎麼辦呢?

一個很自然的想法就是,能否把數據分割成多份保存在不同的機器上?這不就是分區的作用嗎?其實就是解決伸縮性的問題,每個partition都可以放在獨立的服務器上。

當然優勢不僅於此,也可以提高吞吐量。kafka只允許單個partition的數據被一個consumer線程消費。因此,在consumer端,consumer并行度完全依賴於被消費的分區數量。綜上所述,通常情況下,在一個Kafka集群中,partition的數量越多,意味着可以到達的吞吐量越大。

partition結構

每個partition對應於一個文件夾,該文件夾下存儲該partition的數據和索引文件。

如圖所示,可以看到兩個文件夾,都對應着一個叫做asd的topic,在該台服務器上有兩個分區,0和2,那麼1呢?在其他服務器上啦!畢竟是分佈式分佈的!

我們進去asd-0目錄中看看是什麼?有後綴為.index和.log的文件,他們就是該partition的數據和索引文件:

現在先不管它們是何方神聖,因為我會在【分區機制原理】這篇文章中詳細描述。

partition順序性

現在,我需要你睜大眼睛看看關於分區非常重要的一點:

【每個partition內部保證消息的順序。但是分區之間是不保證順序的】

這一點很重要,例如kafka中的消息是某個業務庫的數據,mysql binlog是有先後順序的,10:01分我沒有付款,所以pay_date為null,而10:02分我付款了,pay_date被更新了。

但到了kafka那,由於是分佈式的,多分區的,可就不一定能保證順序了,也許10:02分那條先來,這樣可就會引發嚴重生產問題了。因此,一般我們需要按表+主鍵來分區。保證同一主鍵的數據發送到同一個分區中。

如果你想要 kafka 中的所有數據都按照時間的先後順序進行存儲,那麼可以設置分區數為 1。

Replica (副本)

每個partition可以配置若干個副本。Kafka 定義了兩類副本:領導者副本(Leader Replica)和追隨者副本(Follower Replica)。只能有 1 個領導者副本和 N-1 個追隨者副本。

為啥要用副本?也很好理解,反問下自己為什麼重要的文件需要備份多份呢?備份機制(Replication)是實現高可用的一個手段。

需要注意的是:僅Leader Replica對外提供服務,與客戶端程序進行交互,生產者總是向領導者副本寫消息,而消費者總是從領導者副本讀消息。而Follower Replica不能與外界進行交互,它只做一件事:向領導者副本發送請求,請求領導者把最新生產的消息發給它,保持與領導者的同步。

如果對於剛剛所說的主題、分區、副本還有疑惑,那麼結合下面這張圖再思考一下,我相信你就可以玩轉它了:

下圖所示,TopicA,具有三個partition,每個partion都有1 個leader副本和 1 個follower者副本。為了保證高可用性,一台機器宕機不會有影響,因此leader副本和follower副本必然分佈在不同的機器上。

消息層

Kafka的官方定義是message system,由此我們可以知道Kafka 中最基本的數據單元無疑是消息message,它可理解成數據庫里的一條行或者一條記錄。消息是由字符數組組成。關於消息你必須知道這幾件事:

消息key

發送消息的時候指定 key,這個 key 也是個字符數組。key 用來確定消息寫入分區時,進入哪一個分區。你可以用有明確業務含義的字段作為key,比如用戶號,這樣就可以保證同一個用戶號進入同一個分區。

批量寫入

為了提高效率, Kafka 以批量batch的方式寫入。

一個 batch 就是一組消息的集合, 這一組的數據都會進入同一個 topic 和 partition(這個是根據 producer 的配置來定的) 。

每一個消息都進行一次網絡傳輸會很消耗性能,因此把消息收集到一起再同時處理就高效的多。

當然,這樣會引入更高的延遲以及吞吐量:batch 越大,同一時間處理的消息就越多。batch 通常都會進行壓縮,這樣在傳輸以及存儲的時候效率都更高一些。

位移
生產者向分區寫入消息,每條消息在分區中的位置信息由一個叫位移(Offset)的數據來表徵。分區位移總是從 0 開始,假設一個生產者向一個空分區寫入了 10 條消息,那麼這 10 條消息的位移依次是 0、1、2、…、9。

服務端

Kafka 的服務器端由被稱為 Broker 的服務進程構成,即一個 Kafka 集群由多個 Broker 組成,Kafka支持水平擴展,broker數量越多,集群吞吐量越高。在集群中每個broker都有一個唯一brokerid,不得重複。Broker 負責接收和處理客戶端發送過來的請求,以及對消息進行持久化。

一般會將不同的 Broker 分散運行在不同的機器上,這樣如果集群中某一台機器宕機,kafka可以自動選舉出其他機器上的 Broker 繼續對外提供服務。這其實就是 Kafka 提供高可用的手段之一。

controller

Kafka集群中會有一個或者多個broker,其中有且僅有一個broker會被選舉為控制器(Kafka Controller),它負責管理整個集群中所有分區和副本的狀態。

當某個分區的leader副本出現故障時,由控制器負責為該分區選舉新的leader副本。當檢測到某個分區的ISR集合發生變化時,由控制器負責通知所有broker更新其元數據信息。當為某個topic增加分區數量時,同樣還是由控制器負責分區的重新分配。

這幾句話可能會讓你覺得困惑不要方 只是突出下控制器的職能很多,而這些功能的具體細節會在後面的文章中做具體的介紹。

Kafka中的控制器選舉的工作依賴於Zookeeper,成功競選為控制器的broker會在Zookeeper中創建/controller這個臨時(EPHEMERAL)節點,此臨時節點的內容參考如下:

其中version在目前版本中固定為1,brokerid表示稱為控制器的broker的id編號,timestamp表示競選稱為控制器時的時間戳。

兩種客戶端

Kafka有兩種客戶端。生產者和消費者。我們把生產者和消費者統稱為客戶端(Clients)。

向主題Topic發布消息Message的客戶端應用程序稱為生產者(Producer),生產者程序通常持續不斷地向一個或多個主題發送消息。

而訂閱這些主題消息的客戶端應用程序就被稱為消費者(Consumer)。和生產者類似,消費者也能夠同時訂閱多個主題的消息。

Producer

Producer 用來創建Message。在發布訂閱系統中,他們也被叫做 Publisher 發布者或 writer 寫作者。

通常情況下,會發布到特定的Topic,並負責決定發布到哪個分區(通常簡單的由負載均衡機制隨機選擇,或者通過key,或者通過特定的分區函數選擇分區。)
Producer分為Sync Producer 和 Aync Producer。

Sync Producer同步的生產者,即一定要某條消息成功才會發送下一條。所以它是低吞吐率、一般不會出現數據丟失。

Aync Producer異步的生產者,有個隊列的概念,是直接發送到隊列裏面,批量發送。高吞吐率、可能有數據丟失的。

Consumer 和 Consumer Group

消費者

Consumer 讀取消息。在發布訂閱系統中,也叫做 subscriber 訂閱者或者 reader 閱讀者。消費者訂閱一個或者多個主題,然後按照順序讀取主題中的數據。

消費位移

消費者需要記錄消費進度,即消費到了哪個分區的哪個位置上,這是消費者位移(Consumer Offset)。注意,這和上面所說的消息在分區上的位移完全不是一個概念。上面的“位移”表徵的是分區內的消息位置,它是不變的,即一旦消息被成功寫入到一個分區上,它的位移值就是固定的了。

而消費者位移則不同,它可能是隨時變化的,畢竟它是消費者消費進度的指示器嘛。通過存儲最後消費的 Offset,消費者應用在重啟或者停止之後,還可以繼續從之前的位置讀取。保存的機制可以是 zookeeper,或者 kafka 自己。

消費者組

ConsumerGroup:消費者組,指的是多個消費者實例組成一個組來消費一組主題,分區只能被消費者組中的其中一個消費者去消費,組員之間不能重複消費。

為什麼要引入消費者組呢?主要是為了提升消費者端的吞吐量。多個消費者實例同時消費,加速整個消費端的吞吐量(TPS)。

當然它的作用不僅僅是瓜分訂閱主題的數據,加速消費。它們還能彼此協助。假設組內某個實例掛掉了,Kafka 能夠自動檢測到,然後把這個 Failed 實例之前負責的分區轉移給其他活着的消費者,這個過程稱之為重平衡(Rebalance)。

你務必先把這個詞記住,它是kafka大名鼎鼎的重平衡機制,生產出現的異常問題很多都是由於它導致的。後續我會在【kafka大名鼎鼎又臭名昭著的重平衡】文章中詳細分析。

Zookeeper

zookeeper目前在kafka中扮演着舉重輕重的角色和作用~是kafka不可缺少的一個組件。

目前,Apache Kafka 使用 Apache ZooKeeper 來存儲它的元數據,比如brokers信息、分區的位置和主題的配置等數據就是存儲在 ZooKeeper 集群中。

注意我的用詞,我只說是目前。why?在 2019 年社區提出了一個計劃,以打破這種依賴關係,並將元數據管理引入 Kafka 本身。因為擁有兩個系統會導致大量的重複。

在之前的設計中,我們至少需要運行三個額外的 Java 進程,有時甚至更多。事實上,我們經常看到具有與 Kafka 節點一樣多的 ZooKeeper 節點的 Kafka 集群!此外,ZooKeeper 中的數據還需要緩存在 Kafka 控制器上,這導致了雙重緩存。

更糟糕的是,在外部存儲元數據限制了 Kafka 的可伸縮性。當 Kafka 集群啟動時,或者一個新的控制器被選中時,控制器必須從 ZooKeeper 加載集群的完整狀態。隨着元數據數量的增加,加載過程需要的時間也會增加,這限制了 Kafka 可以存儲的分區數量。

最後,將元數據存儲在外部會增加控制器的內存狀態與外部狀態不同步的可能性。

因此,未來,Kafka 的元數據將存儲在 Kafka 本身中,而不是存儲在 ZooKeeper 之類的外部系統中。可以持續關注kafka社區動態哦!

總結

一個典型的kafka集群包含若干個producer(向主題發布新消息),若干consumer(從主題訂閱新消息,用Consumer Offset表徵消費者消費進度),cousumergroup(多個消費者實例共同組成的一個組,共同消費多個分區),若干broker(服務器端進程)。還有zookeeper。

kafka發布訂閱的對象叫主題,每個Topic下可以有多個Partition,Partition中每條消息的位置信息又叫做消息位移(Offset),Partition有副本機制,使得同一條消息能夠被拷貝到多個地方以提供數據冗餘,副本分為領導者副本和追隨者副本。

可以用下面這張圖來形象表達kafka的組成:

另外,再po一張思維導圖助你回顧本文所述的術語。

重要!!關注【胖滾豬學編程】公眾號發送”kafka”。獲取本文所有架構圖以及Kafka全系列思維導圖!

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

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

分類
發燒車訊

滅絕時代將來臨? WWF:50年來全球野生動物數量驟減2/3

摘錄自2020年9月16日民視新聞報導

全球最大的非政府環境保護組織「世界自然基金會」,最近發布了地球生命力報告2020,顯示近半世紀來,全球野生動物種群數量,已平均銳減68%,生物多樣性消失,人類難辭其咎,像是巴西中西部大沼澤的林火,疑似是人為的火災演變成的森林大火,目前就燒毀了70%美洲虎的主要棲息地。

中南美洲的物種及全球淡水棲息地受到的衝擊尤其嚴重,平均分別下降了94%和84%,世界自然基金會總幹事藍柏堤尼表示,「不到50年我們就看到銳減2/3的野生動物,相較於這些動物棲息在地球上數百萬年,這不過是一眨眼的工夫,第二個原因必須擔憂的是,我們看到過去20年來加速惡化,上次地球生命力報告發布時說的是近6成,但現在是7成。」

生物多樣性
國際新聞

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

【其他文章推薦】

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

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

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

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

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

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