PHP 的 self 關鍵字用法

之前有人詢問 self 關鍵字的用法,答案是比較明顯的:靜態成員函數內不能用 this 調用非成員函數,但可以用 self 調用靜態成員函數/變量/常量;其他成員函數可以用 self 調用靜態成員函數以及非靜態成員函數。隨着討論的深入,發現 self 並沒有那麼簡單。鑒於此,本文先對幾個關鍵字做對比和區分,再總結 self 的用法。

parentstatic 以及 this 的區別

要想將徹底搞懂 self ,要與 parentstatic 以及 this 區分開。以下分別做對比。

parent

selfparent 的區分比較容易: parent 引用父類/基類被隱蓋的方法(或變量), self則引用自身方法(或變量)。例如構造函數中調用父類構造函數:

class Base {
    public function __construct() {
        echo "Base contructor!", PHP_EOL;
    }
}

class Child {
    public function __construct() {
        parent::__construct();
        echo "Child contructor!", PHP_EOL;
    }
}

new Child;
// 輸出:
// Base contructor!
// Child contructor!

 

static

static 常規用途是修飾函數或變量使其成為類函數和類變量,也可以修飾函數內變量延長其生命周期至整個應用程序的生命周期。但是其與 self 關聯上是PHP 5.3以來引入的新用途:靜態延遲綁定。

有了 static 的靜態延遲綁定功能,可以在運行時動態確定歸屬的類。例如:

class Base {
    public function __construct() {
        echo "Base constructor!", PHP_EOL;
    }

    public static function getSelf() {
        return new self();
    }

    public static function getInstance() {
        return new static();
    }

    public function selfFoo() {
        return self::foo();
    }

    public function staticFoo() {
        return static::foo();
    }

    public function thisFoo() {
        return $this->foo();
    }

    public function foo() {
        echo  "Base Foo!", PHP_EOL;
    }
}

class Child extends Base {
    public function __construct() {
        echo "Child constructor!", PHP_EOL;
    }

    public function foo() {
        echo "Child Foo!", PHP_EOL;
    }
}

$base = Child::getSelf();
$child = Child::getInstance();

$child->selfFoo();
$child->staticFoo();
$child->thisFoo();

 

程序輸出結果如下:

Base constructor!
Child constructor!
Base Foo!
Child Foo!
Child Foo!

 

在函數引用上, selfstatic 的區別是:對於靜態成員函數, self 指向代碼當前類, static 指向調用類;對於非靜態成員函數, self 抑制多態,指向當前類的成員函數, static 等同於 this ,動態指向調用類的函數。

parentselfstatic 三個關鍵字聯合在一起看挺有意思,分別指向父類、當前類、子類,有點“過去、現在、未來”的味道。

this

selfthis 是被討論最多,也是最容易引起誤用的組合。兩者的主要區別如下:

  1. this 不能用在靜態成員函數中, self 可以;
  2. 對靜態成員函數/變量的訪問, 建議 用 self ,不要用 $this::$this-> 的形式;
  3. 對非靜態成員變量的訪問,不能用 self ,只能用 this ;
  4. this 要在對象已經實例化的情況下使用, self 沒有此限制;
  5. 在非靜態成員函數內使用, self 抑制多態行為,引用當前類的函數;而 this 引用調用類的重寫(override)函數(如果有的話)。

self 的用途

看完與上述三個關鍵字的區別, self 的用途是不是呼之即出?一句話總結,那就是: self總是指向“當前類(及類實例)”。詳細說則是:

  1. 替代類名,引用當前類的靜態成員變量和靜態函數;
  2. 抑制多態行為,引用當前類的函數而非子類中覆蓋的實現;

槽點

  1. 這幾個關鍵字中,只有 this 要加 $ 符號且必須加,強迫症表示很難受;
  2. 靜態成員函數中不能通過 $this-> 調用非靜態成員函數,但是可以通過 self:: 調用,且在調用函數中未使用 $this-> 的情況下還能順暢運行。此行為貌似在不同PHP版本中表現不同,在當前的7.3中ok;
  3. 在靜態函數和非靜態函數中輸出 self ,猜猜結果是什麼?都是 string(4) "self" ,迷之輸出;
  4. return $this instanceof static::class; 會有語法錯誤,但是以下兩種寫法就正常:
    $class = static::class;
    return $this instanceof $class;
    // 或者這樣:
    return $this instanceof static;

 

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

特斯拉發表電動卡車 Cybertruck,一台從科幻電影走出來的鋼鐵車

今天的 Elon Musk 看起來不像鋼鐵人,更像蝙蝠俠,因為他們的新車 Cybertruck,不但外型酷似蝙蝠車,同時還能防彈防撞,並且擁有超越保時捷的加速度,跟勝過市面上卡車的拖吊能力,更驚人的是,售價只要 39,900 美元起。

眾所期待的特斯拉新車 Cybertruck 今日正式發表,和之前流出的影像不同,Cybertruck 酷似隱形戰機 F-117 的設計,讓人聯想到蝙蝠車,甚至懷疑這是不是一台防雷達偵測的戰車?

Tesla Cybertruck 全車採用冷鑄鋼板,能夠抵擋 9mm 口徑手槍的射擊,現場展示用重鎚敲擊也毫髮無傷;車窗玻璃同樣採用防彈設計,然而有趣的是,現場展示時,被大鐵球砸出了一片雪花。「至少,它沒被打穿,你坐在裡面很安全。」Elon Musk 笑著說。

Cybertruck 為了因應負重,搭載了適應性氣壓懸吊系統,針對高速公路,或是越野泥巴路,能夠自動調整懸吊高度,同時也順便使用這個氣壓系統,做了一個高壓出力裝置,使用者可以自行加裝不同氣壓工具,像是高壓水槍或是電鑽等。

當重裝電動機車開上後廂時,懸吊系統會自動調整車尾高度,讓車身保持平衡。

車尾與其他皮卡車開放式貨斗不同,Cybertruck 採用封閉式貨斗,並有升降式尾門,現場展示時,將這台電動機車 ATV 直接騎上貨斗後,還能直接充電,顯然是在致敬蝙蝠車跟蝙蝠機車。

Cybertruck 如同其他皮卡車,車尾裝有釣鉤,能夠充當拖車使用,而歸功於它的強力馬達,拖車能力屌打了皮卡車霸主 Ford F-150,在現場展示的影片中,特斯拉讓 Cybertruck 跟 F-150 互相拖住對方,進行拔河測試,結果 F-150 整台被 Cybertruck 拖走。

F-150 慘遭 Cybertruck 拖走。

馬斯克強調,一般皮卡車需要另外裝載發電機才能使用電動工具,Cybertruck 直接提供了電源,因此省下不少空間,同時還提供強大的拖力。

此外,做為一台卡車,Cybertruck 莫名其妙地擁有超越保時捷的加速度,根據現場公布數據,最頂級版的 0-100 公里加速時間不到 3 秒。現場展示了 Cybertruck 與 Porsche 911 賽跑的影片,起步雖然小輸一點,但隨後就超越了 911。

現場展示競速影片,大約 1 秒後,Cybertruck 就超過了 911。

Tesla Cybertruck 共有 3 種版本,依照馬達數量來分別,最低價 39,900 美元起,最高 69,900 美元。Cybertruck 從今天起在美國開放預購,實際交車時間預計要等到 2021 年底。頂級的三馬達款,更預計要等到 2022 年底才會開始生產。

如同馬斯克開場所說,卡車在過去幾十年來都長得差不多,特斯拉要打造一台完全不一樣的卡車,同時還要保持零排放,跟超高性能,從今天的現場展示來看,特斯拉再次完成一個不可能的任務。在興奮之餘也別忘了,這一切都是現場展示,實際上如何,就有待實際交車後驗證了!

(合作媒體:。圖片來源:)

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

【其他文章推薦】

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

【其他文章推薦】

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

深入理解static關鍵字

在開始講static之前,我想讓各位看一段有意思的代碼:

public class Test {
     
    static{
        System.out.println("test static 1");
    }
  
    static{
        System.out.println("test static 2");
    }
    
    public static void main(String[] args) {
         
    }
}

看完程序,小白童鞋發話了:啥玩意?main方法中啥都沒有,能運行啥?博主你個星星星…

運行結果:
test static 1
test static 2

小白童鞋:那啥…那啥…博主我說啥了,我啥都沒說…

其實,上面的代碼懂的自然懂,不懂的自然就不懂了,因為上面的代碼涉及到JVM的類加載了!當然不在本篇博客文章的範疇內,如果有興趣理解上面的程序,這篇文章可能會對你有所幫助

1、static存在的主要意義

static的主要意義是在於創建獨立於具體對象的域變量或者方法。以致於即使沒有創建對象,也能使用屬性和調用方法

static關鍵字還有一個比較關鍵的作用就是 用來形成靜態代碼塊以優化程序性能。static塊可以置於類中的任何地方,類中可以有多個static塊。在類初次被加載的時候,會按照static塊的順序來執行每個static塊,並且只會執行一次。

  為什麼說static塊可以用來優化程序性能,是因為它的特性:只會在類加載的時候執行一次。因此,很多時候會將一些只需要進行一次的初始化操作都放在static代碼塊中進行。

2、static的獨特之處

1、被static修飾的變量或者方法是獨立於該類的任何對象,也就是說,這些變量和方法不屬於任何一個實例對象,而是被類的實例對象所共享

怎麼理解 “被類的實例對象所共享” 這句話呢?就是說,一個類的靜態成員,它是屬於大夥的【大夥指的是這個類的多個對象實例,我們都知道一個類可以創建多個實例!】,所有的類對象共享的,不像成員變量是自個的【自個指的是這個類的單個實例對象】…我覺得我已經講的很通俗了,你明白了咩?

2、在該類被第一次加載的時候,就會去加載被static修飾的部分,而且只在類第一次使用時加載並進行初始化,注意這是第一次用就要初始化,後面根據需要是可以再次賦值的。

3、static變量值在類加載的時候分配空間,以後創建類對象的時候不會重新分配。賦值的話,是可以任意賦值的!

4、被static修飾的變量或者方法是優先於對象存在的,也就是說當一個類加載完畢之後,即便沒有創建對象,也可以去訪問。

3、static應用場景

因為static是被類的實例對象所共享,因此如果某個成員變量是被所有對象所共享的,那麼這個成員變量就應該定義為靜態變量

因此比較常見的static應用場景有:

1、修飾成員變量
2、修飾成員方法
3、靜態代碼塊
4、修飾類【只能修飾內部類也就是靜態內部類】
5、靜態導包

以上的應用場景將會在下文陸續講到…

4、靜態變量和實例變量的概念

靜態變量:
static修飾的成員變量叫做靜態變量【也叫做類變量】,靜態變量是屬於這個類,而不是屬於是對象。

實例變量:
沒有被static修飾的成員變量叫做實例變量,實例變量是屬於這個類的實例對象。

還有一點需要注意的是:static是不允許用來修飾局部變量,不要問我問什麼,因為java規定的!

5、靜態變量和實例變量區別【重點常用】

靜態變量:
靜態變量由於不屬於任何實例對象,屬於類的,所以在內存中只會有一份,在類的加載過程中,JVM只為靜態變量分配一次內存空間。

實例變量:
每次創建對象,都會為每個對象分配成員變量內存空間,實例變量是屬於實例對象的,在內存中,創建幾次對象,就有幾份成員變量。

我相信各位智商都比宜春智商要高,應該都能理解上面的話。下面舉了例子完全出於娛樂,理解了大可不必看,下面的例子僅供參考,僅供娛樂一下下氣氛,趕時間的熊dei大可略過!

怎麼理解呢?打個比喻吧…就比方說程序員小王是一個比較溫柔陽光的男孩子,這1024的這一天,老闆閑的沒事,非要拉着程序員小王來玩耍,怎麼個玩法呢?老闆和小王一人拿着一把菜刀,規則很簡單,互相傷害,一人一刀,你一刀,我一刀….遊戲一開始,老闆二話不說,跳起來就是一刀,程序員小王二話也沒說反手就是一菜刀回去,這個時候老闆發飆了,雙眼瞪得忒大,跳起來又是一刀,這個時候程序員小王不敢還手了,就沒動手。沒想到老闆越來越生猛,左一刀右一刀全程下來差不多砍個半個時….程序員小王一直沒有還過手,因為小王知道他是老闆…

這個程序員小王只會在老闆第一次揮刀的時候,回老闆一刀,之後就不還手了,這個時候我們把程序員小王看做是靜態變量,把老闆第一次向小王揮刀看做是類加載,把小王回老闆一刀看出是分配內存空間,而一人一刀這個回合過程看成是類加載的過程,之後老闆的每一刀都看成是創建一次對象。

連貫起來就是static變量值在類第一次加載的時候分配空間,以後創建類對象的時候不會重新分配

之後這個老闆挨了一刀之後躺醫院了一年,一出院回到公司第一件事就是拉程序員宜春出來玩耍,老闆殊不知其然,這個博主程序員宜春性格異常暴躁,老闆遞給程序員宜春一把菜刀,博主宜春一接過菜刀,猝不及防的被老闆跳起來就是一刀,程序員宜春痛的嗷了一聲,暴躁的程序員宜春還沒嗷完,在嗷的同時跳起來就是給老闆一刀,接着老闆跳起來又是一刀,程序員宜春嗷的一聲又是回一刀,老闆跳起來又一刀,程序員宜春嗷的一聲又是回一刀,只要老闆沒停程序員宜春就沒停,因為程序員宜春知道,就自己這曝脾氣,暴躁起來si都敢摸,肯定有幾個老鐵知道….

程序員宜春就類似實例變量,每次創建對象,都會為每個對象分配成員變量內存空間,就像老闆來一刀,程序員宜春都會回一刀這樣子的…

6、訪問靜態變量和實例變量的兩種方式

我們都知道靜態變量是屬於這個類,而不是屬於是對象,static獨立於對象。

但是各位有木有想過:靜態成員變量雖然獨立於對象,但是不代表不可以通過對象去訪問,所有的靜態方法和靜態變量都可以通過對象訪問【只要訪問權限足夠允許就行】,不理解沒關係,來個代碼就理解了

public class StaticDemo {

        static int value = 666;

        public static void main(String[] args) throws Exception{
            new StaticDemo().method();
        }

        private void method(){
            int value = 123;
            System.out.println(this.value);
        }

}

猜想一下結果,我猜你的結果是123,哈哈是咩?其實

運行結果: 666

回過頭再去品味一下上面的那段話,你就能非常客觀明了了,這個思想概念要有隻是這種用法不推薦!

因此小結一下訪問靜態變量和實例變量的兩種方法:

靜態變量:

類名.靜態變量

對象.靜態變量(不推薦)

靜態方法:

類名.靜態方法

對象.靜態方法(不推薦)

7、static靜態方法

static修飾的方法也叫做靜態方法,不知道各位發現咩有,其實我們最熟悉的static靜態方法就是main方法了~小白童鞋:喔好像真的是哦~。由於對於靜態方法來說是不屬於任何實例對象的,this指的是當前對象,因為static靜態方法不屬於任何對象,所以就談不上this了。

還有一點就是:構造方法不是靜態方法

8、static靜態代碼塊

先看個程序吧,看看自個是否掌握了static代碼塊,下面程序代碼繼承關係為 BaseThree——> BaseTwo——> BaseOne

BaseOne類

package com.gx.initializationblock;

public class BaseOne {

    public BaseOne() {
        System.out.println("BaseOne構造器");
    }

    {
        System.out.println("BaseOne初始化塊");
        System.out.println();
    }

    static {
        System.out.println("BaseOne靜態初始化塊");

    }

}

BaseTwo類

package com.gx.initializationblock;

public class BaseTwo extends BaseOne {
    public BaseTwo() {
        System.out.println("BaseTwo構造器");
    }

    {
        System.out.println("BaseTwo初始化塊");
    }

    static {
        System.out.println("BaseTwo靜態初始化塊");
    }
}

BaseThree 類

package com.gx.initializationblock;

public class BaseThree extends BaseTwo {
    public BaseThree() {
        System.out.println("BaseThree構造器");
    }

    {
        System.out.println("BaseThree初始化塊");
    }

    static {
        System.out.println("BaseThree靜態初始化塊");
    }
}

測試demo2類

package com.gx.initializationblock;

/*
     注:這裏的ABC對應BaseOne、BaseTwo、BaseThree 
 * 多個類的繼承中初始化塊、靜態初始化塊、構造器的執行順序
     在繼承中,先後執行父類A的靜態塊,父類B的靜態塊,最後子類的靜態塊,
     然後再執行父類A的非靜態塊和構造器,然後是B類的非靜態塊和構造器,最後執行子類的非靜態塊和構造器
 */
public class Demo2 {
    public static void main(String[] args) {
        BaseThree baseThree = new BaseThree();
        System.out.println("-----");
        BaseThree baseThree2 = new BaseThree();

    }
}

運行結果

BaseOne靜態初始化塊
BaseTwo靜態初始化塊
BaseThree靜態初始化塊
BaseOne初始化塊

BaseOne構造器
BaseTwo初始化塊
BaseTwo構造器
BaseThree初始化塊
BaseThree構造器
-----
BaseOne初始化塊

BaseOne構造器
BaseTwo初始化塊
BaseTwo構造器
BaseThree初始化塊
BaseThree構造器

至於static代碼塊運行結果不是很清晰的童鞋,詳細講解請看這篇

以上僅僅是讓各位明確代碼塊之間的運行順序,顯然還是不夠的,靜態代碼塊通常用來對靜態變量進行一些初始化操作,比如定義枚舉類,代碼如下:

public enum WeekDayEnum {
    MONDAY(1,"周一"),
    TUESDAY(2, "周二"),
    WEDNESDAY(3, "周三"),
    THURSDAY(4, "周四"),
    FRIDAY(5, "周五"),
    SATURDAY(6, "周六"),
    SUNDAY(7, "周日");
 
    private int code;
    private String desc;
 
    WeekDayEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }
 
    private static final Map<Integer, WeekDayEnum> WEEK_ENUM_MAP = new HashMap<Integer, WeekDayEnum>();
 
    // 對map進行初始化
    static {
        for (WeekDayEnum weekDay : WeekDayEnum.values()) {
            WEEK_ENUM_MAP.put(weekDay.getCode(), weekDay);
        }
    }
 
    public static WeekDayEnum findByCode(int code) {
        return WEEK_ENUM_MAP.get(code);
    }
 
    public int getCode() {
        return code;
    }
 
    public void setCode(int code) {
        this.code = code;
    }
 
    public String getDesc() {
        return desc;
    }
 
    public void setDesc(String desc) {
        this.desc = desc;
    }
} 

當然不僅僅是枚舉這一方面,還有我們熟悉的單例模式同樣也用到了靜態代碼塊,如下:

public class Singleton {
    private static Singleton instance;
 
    static {
        instance = new Singleton();
    }
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        return instance;
    }
}

9、static變量與普通變量區別

static變量也稱作靜態變量,靜態變量和非靜態變量的區別是:靜態變量被所有的對象所共享,在內存中只有一個副本,它當且僅當在類初次加載時會被初始化。而非靜態變量是對象所擁有的,在創建對象的時候被初始化,存在多個副本,各個對象擁有的副本互不影響。

還有一點就是static成員變量的初始化順序按照定義的順序進行初始化。

10、靜態內部類

靜態內部類與非靜態內部類之間存在一個最大的區別,我們知道非靜態內部類在編譯完成之後會隱含地保存着一個引用,該引用是指向創建它的外圍內,但是靜態內部類卻沒有。沒有這個引用就意味着:

1、它的創建是不需要依賴外圍類的創建。
2、它不能使用任何外圍類的非static成員變量和方法。

代碼舉例(靜態內部類實現單例模式)

public class Singleton {
    
   // 聲明為 private 避免調用默認構造方法創建對象
    private Singleton() {
    }
    
   // 聲明為 private 表明靜態內部該類只能在該 Singleton 類中被訪問
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Singleton 類加載時,靜態內部類 SingletonHolder 沒有被加載進內存。只有當調用 getUniqueInstance()方法從而觸發 SingletonHolder.INSTANCESingletonHolder 才會被加載,此時初始化 INSTANCE 實例,並且 JVM 能確保 INSTANCE 只被實例化一次。

這種方式不僅具有延遲初始化的好處,而且由 JVM 提供了對線程安全的支持。

11、靜態導包

靜態導包格式:import static

這兩個關鍵字連用可以指定導入某個類中的指定靜態資源,並且不需要使用類名調用類中靜態成員,可以直接使用類中靜態成員變量和成員方法

//  Math. --- 將Math中的所有靜態資源導入,這時候可以直接使用裏面的靜態方法,而不用通過類名進行調用
//  如果只想導入單一某個靜態方法,只需要將換成對應的方法名即可
 
import static java.lang.Math.;
//  換成import static java.lang.Math.max;具有一樣的效果
 
public class Demo {
    public static void main(String[] args) {
 
        int max = max(1,2);
        System.out.println(max);
    }
}

靜態導包在書寫代碼的時候確實能省一點代碼,可以直接調用裏面的靜態成員,但是會影響代碼可讀性,所以開發中一般情況下不建議這麼使用。

12、static注意事項

1、靜態只能訪問靜態。
2、非靜態既可以訪問非靜態的,也可以訪問靜態的。

13、final與static的藕斷絲連

到這裏文章本該結束了的,但是static的使用始終離不開final字眼,二者可謂藕斷絲連,常常繁見,我覺得還是很有必要講講,那麼一起來看看下面這個程序吧。

package Demo;

class FinalDemo {
    public final double i = Math.random();
    public static double t = Math.random();
}

public class DemoDemo {
    public static void main(String[] args) {

        FinalDemo demo1 = new FinalDemo();
        FinalDemo demo2 = new FinalDemo();
        System.out.println("final修飾的  i=" + demo1.i);
        System.out.println("static修飾的 t=" + demo1.t);
        System.out.println("final修飾的  i=" + demo2.i);
        System.out.println("static修飾的 t=" + demo2.t);

        System.out.println("t+1= "+ ++demo2.t );
//      System.out.println( ++demo2.i );//編譯失敗
      }
}
運行結果:
    final修飾的  i=0.7282093281367935
    static修飾的 t=0.30720545678577604
    final修飾的  i=0.8106990945706758
    static修飾的 t=0.30720545678577604
    t+1= 1.307205456785776

static修飾的變量沒有發生變化是因為static作用於成員變量只是用來表示保存一份副本,其不會發生變化。怎麼理解這個副本呢?其實static修飾的在類加載的時候就加載完成了(初始化),而且只會加載一次也就是說初始化一次,所以不會發生變化!

至於final修飾的反而發生變化了?是不是巔覆你對final的看法?關於final詳細講解博主也準備好了一篇文章

ok,文章就先到這裏了,希望這篇文章能夠幫助到你對static的認識,若有不足或者不正之處,希望諒解並歡迎批評指正!

如果本文章對你有幫助,哪怕是一點點,那就請點一個讚唄,謝謝~

參考:
《java編程思想》

如果本文對你有一點點幫助,那麼請點個讚唄,謝謝~

最後,若有不足或者不正之處,歡迎指正批評,感激不盡!如果有疑問歡迎留言,絕對第一時間回復!

歡迎各位關注我的公眾號,一起探討技術,嚮往技術,追求技術,說好了來了就是盆友喔…

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

京東物流出問題了?褥了30塊羊毛 & 淺析系統架構

本人親身經歷,但後續的流程分析都是個人猜測的,畢竟沒有實際做過這塊的業務。

訂單物流阻塞經過

火熱的雙11剛剛退去,截止今日,我在京東購買的礦泉水終於到貨啦,下單兩箱還只收到了一箱 🙁 ,從下單到收到貨過去了14天,足足兩周的時間。

我從11-20號開始與京東客服聯繫,直到11-25整個購物體驗才完成,也因為京東沒有按照約定重新發貨,算是補償了我3000個京豆。

朋友們,不會不知道京豆是幹啥的吧,100個京豆相當於一塊錢,1000個京豆相當於10塊錢,3000個京豆就是30塊錢。

可那不是現金有啥卵用,你不會不在京東購物吧,下單的時候就可以選擇用京豆來抵用一部分下單金額了。

所以一般購買商品后鼓勵你去評價,文字超過一定字數且上傳了購買商品的圖片,就能得到比如20個京豆。京豆積少成多,就可以下單抵用現金了。

廢話不多說,回到正題!

雙11我在京東下單,自營商品的訂單一般都是次日達,因為雙11物流緊張,所以下單后提示11-13日送達。

11-13日:

遺憾的是,11-13日並沒有如期送達,查看訂單物流,增加了一段溫馨提示:「由於銷售火爆,根據目前情況,訂單預計11月16日送達到您的手中」,額~,當然大家都能理解,原來京東商品這麼「火爆」,畢竟雙十一累積銷量2000億呢。

11-20日 周三:

問題是到了11-16日並沒有送達,我把這個訂單差點忘記了,11-20號突然想起來了這件事,上京東確認了訂單,才發現還是那個「銷售火爆…」的提示呀!竟然沒有給我送貨。。。

然後在線聯繫人工客服,說了一下情況,客服態度很好,兩箱水拆分下單后因已經拆分為了兩個訂單,有兩個訂單號,為了表示歉意,每個訂單號補了500個京豆,1000個京豆到手了。

然後,客服跟我說,已經給我催促倉儲發貨了,讓耐心等待一下,預計第二天就能到了。

11-21號 周四:

可惜到了第二天,並沒有像客服MM所說的那樣如期送達,反正已經晚了,心想也不差這一天兒,還贈送了京豆,再等等了…… 。

11-22號 周五:

然而,到了11-22號還是沒有配送物流通知,訂單中的分揀流程沒有完成,這是什麼操作??

當天繼續聯繫客服,問了是什麼原因,又來「話術」:小妹已為您催促正在發貨中,此時我有點懷疑了,可能這個流程本身就中斷了,需要人工來協助處理補單流程。

此刻,「客服的嘴,騙人的鬼」終於用到這了~

同時,京東客服升級來了個電話溝通,誠摯的表示歉意,說是倉儲這邊發貨有點問題,正在重新補貨中… ,預計明日就能送到,請注意查收!

11-23日 周六:

「客服的嘴,騙人的鬼」再一次用到這了~

周六仍然沒有收到貨,而且訂單里的物流配送流程一動也沒動~

11-23日 周日:

周日仍然沒有收到貨,而且訂單里的物流配送流程一動也沒動~

看來沒很好的注重用戶體驗嘛,再次在線聯繫客服,每次接線的不是同一個客服,所以每次都要求提供一下訂單號,很煩,此時很無語了,本用戶表示很生氣啊,自己查!

然後呢,客服又說已經重新補發貨了,並且這次竟然不給我大概的送貨時間點了,因為他不相信到底有沒有真的去補發貨操作了,補發貨這個操作多半客服是沒有權限的。

另外,解釋到因訂單延遲時間過長,又一次非常的抱歉,給申請了1000京豆,不過這次京豆並不是實時到賬的,需要經過審核流程。

11-25日 周一:

早上已經在地鐵上了,收到了京東快遞小哥的來電,但是只到了一個訂單的貨。查了一下另外一個訂單物流狀態仍然一動沒動 :(。

最後,客服專員再次電話聯繫,解釋到這個訂單給疏忽了,建議我重新下單,然後這個訂單走退款流程。並且再一次給予了1000京豆的補償 :)。

物流系統異常分析:

上述物流配送異常流程中,想了解故障原因,電話中我也有意識的去問一下客服,是不是某個環節有這樣的問題,但是從客服那裡只能給到說倉配流程有問題 ,具體他們也不是很清楚了,全都是針對用戶的話術,避免說錯話。

作為個技術人,通常得思考一下問題背後的原因:

  • 到底是哪個環節出現的問題
  • 出現這樣問題的原因
  • 對用戶的影響及應對方案
  • 如何能避免類似的問題發生
物流系統介紹

由此次問題引出,我還特意去查資料看了一下京東物流的系統架構演進過程。記得當時京東物流招人非常猛,作為一個內部非常重量級的項目投入了很多研發人力。

在2012年的時候京東內部開始對物流系統進行設計改造,那時訪問量應該還不算高,最初的系統還沒那麼複雜。新改造的物流系統:「青龍系統

青龍系統演進過程如上圖所示 ,它的系統發展至今,已經包含了分揀中心,運輸路由,終端,對外拓展,運營支持等五十餘個核心子系統,構建了完善的電商物流體系。

並且青龍系統中總結了一些最佳的實戰原則,如下所示:

這些系統設計原則我認為對任何系統都是通用的,值得我們一起學習的:

  • 高可用

選擇合適的架構方案;大系統小做,服務拆分;併發控制,服務隔離;灰度發布;全方位監控報警;核心服務,平滑降級。

  • 高性能

緩存和異步化,同步接口異步化設計;接口數據緩存化。

接口數據緩存化是非常重要的手段,對Redis緩存系統的很好的利用,構建了具有自己特色的緩存體系,很好的支撐了業務發展。同時,還發展了基於Redis的分佈式調度系統

  • 數據一致性

高實時性/高一致性,高實時性/低一致性,低實時性/高一致性,低實時性/低一致性。

針對具體的業務,可以匹配到具體的數據場景,找到對應的解決方案。要客觀的結合業務分析,選擇最適合的一致性方案,並不是高實時性/高一致性就好,成本是很更貴的。

  • 用戶體驗

東哥要求過任何人不能對用戶體驗提升的建議說No。用戶體驗主要遵循MVP原則和動態運營的原則。

MVP原則:也就是敏捷開發中的迭代思路。即快速迭代,核心需求線上,及時的反饋和改進。

動態運營:跟MVP原則強關聯,上線后收集並分析用戶數據,使得產品落地的設計符合用戶的需求,不符合設計要求的就要不斷的持續調整,是一個動態持續的過程。

物流分揀系統

簡單介紹完了物流系統的演進過程及架構原則,還是回到主題,到底是哪個環節出現了問題?

需要了解整個購物鏈路的各個環節:

用戶整個購物流程經過以上幾個關鍵的流程,已經生成訂單號並且已經支付了,流程到了訂單中心。

各個系統都是分佈式部署的,訂單中心會發送一個MQ消息給各個下游系統,積分系統增加積分京豆等,促銷系統發放優惠券等,倉儲系統接收到MQ消息進行處理,調用物流系統生成物流單,通知到配送站,由配送員送貨。

結合一個火爆的訂單,看一下訂單跟蹤過程:

該訂單在倉庫處理中已經打包完成,訂單在京東【北京李橋接貨倉庫】分揀完成。注意到了「分揀」二字,順便看了一下正常的訂單流程,會經過多個貨倉的分揀過程,最終會分揀到離用戶最近的貨倉。

所以,猜測,這筆訂單的問題就是在配送前的分揀系統處理過程中出現了異常情況。

青龍物流系統其中就包括了預分揀流程,如下所示:

當用戶下單后,首先必須是經過預分揀環節,但是根據最新的訂單跟蹤過程看,是先進行了倉庫打包處理,然後進入分揀流程。

分揀系統接收到訂單,根據不同的訂單進行規則匹配,分配站點,處理成功後生產包裹打印標籤。

訂單無法被正常分揀完成,將無法生成訂單:

想必我的訂單大概率就是在分揀環節出現了問題 ~

分揀系統的目標:

其中可用性要求是達到99.99%,4個9的可用率呢,看來很不幸啊,不可用的0.01%小概率事件偶發在了我的訂單上。

預分揀算法:

1、經驗值

只適用於同一個地址多次購買,依賴於第一妥投地址。

2、特徵值

需要提前人工維護關鍵字,依賴於關鍵字的準確性。

3、特殊配置

需要提前人工配置,依賴於該區域是否有特殊配置。

4、GIS

通過GIS技術精準的匹配地理位置。

上述都沒有匹配到,那麼只能走人工處理流程了。

預分揀系統架構:

訂單系統下發服務,默認會進入到預分揀系統,不同的訂單有不同的匹配規則,匹配規則使用開源的Drools來實現的,規則匹配完成,會按照預分揀算法匹配,優先匹配到離用戶最近的地址,返回自動預分揀的結果。

一旦回傳失敗,應該會有預警,需要人工介入來協助完成預分揀,將結果返回給訂單服務。

預分揀服務系統交互流程:

分揀服務使用Tomcat分佈式部署的Worker進程,完成后,將結果寫入到任務庫,回傳服務從人物庫抓取分揀結果回傳站點。

下圖來源於網絡,不是很清晰了:

其中預分揀服務接受訂單服務都是分佈式部署的,並且針對不同的訂單做了服務隔離,使用應用服務器是Tomcat;全文檢索使用的Solr,可能目前已改進為流行的ElasticSerach架構了;分佈式緩存使用了Redis集群;預分揀算法中的地址庫、特徵值、配置都對應了自己的Worker集群,也是做了服務隔離,每個服務分佈式部署,最終將結果寫入到MySQL數據庫中;預分揀回傳站點單獨的Worker集群,用來從數據庫抓取分揀數據,返回給用戶站點。

小結

經過以上過程猜測性分析,基本就清楚了自己的訂單問題出現的位置了。

大概率就是預分揀服務在某一個站點因為流量洪峰或異常出現了故障,可能服務恢復后沒有及時完成自動分揀數據校對。

與客服的溝通結果來看,當分揀過程出現問題后,可能並沒有及時預警並人工及時的去干預處理,導致分揀流程被阻塞,遲遲無法進入到分揀恢復階段。或許也是考慮到這種小概率事件,就由用戶來直接反饋,然後由人工介入處理。

但是,很明顯,客服用話術告知用戶結果,讓用戶耐心等待的同時。在後續的分揀系統訂單恢複流程並不是那麼順暢的,不一定是那麼簡單的人工直接快速處理,會經過一些校驗核對、人工審核等一系列流程,又或者讓技術人員協助恢復的,導致分揀流程流轉下去很慢,也進而影響了用戶體驗。

在線話術告知用戶結果算是A方案。

人工處理的第一筆訂單跟蹤:

而第二個訂單,客服根據情況執行了B方案,將問題升級到專員,電話聯繫用戶,建議用戶重新下單,並給予一定的補償。當你重新下單,分揀系統接收到新的訂單,就是進入了自動預分揀訂單處理過程了,自動化流程當然是很快的,無需人工干預。

總體來說,京東客服的做法可圈可點,整體售後服務流程較以前值得肯定,越來越完善。

同時,系統架構在未來方向上,肯定更趨向於更加的智能化,使用機器學習、人工智能等手段持續不斷優化物流的各環節,減少或避免小概率的事件發生。

ps:文章前半段真實發生,後半段僅作為問題分析參考。

歡迎關注我的公眾號,掃二維碼關注獲得更多精彩文章,與你一同成長~

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

[apue] 神奇的 Solaris pipe

說到 pipe 大家可能都不陌生,經典的pipe調用配合fork進行父子進程通訊,簡直就是Unix程序的標配。

然而Solaris上的pipe卻和Solaris一樣是個奇葩(雖然Solaris前途黯淡,但是不妨礙我們從它裏面挖掘一些有價值的東西),

有着和一般pipe諸多的不同之處,本文就來說說Solaris上神奇的pipe和一般pipe之間的異同。

 

1.solaris pipe 是全雙工的

一般系統上的pipe調用是半雙工的,只能單向傳遞數據,如果需要雙向通訊,我們一般是建兩個pipe分別讀寫。像下面這樣:

 1     int n, fd1[2], fd2[2]; 
 2     if (pipe (fd1) < 0 || pipe(fd2) < 0)
 3         err_sys ("pipe error"); 
 4 
 5     char line[MAXLINE]; 
 6     pid_t pid = fork (); 
 7     if (pid < 0) 
 8         err_sys ("fork error"); 
 9     else if (pid > 0)
10     {
11         close (fd1[0]);  // write on pipe1 as stdin for co-process
12         close (fd2[1]);  // read on pipe2 as stdout for co-process
13         while (fgets (line, MAXLINE, stdin) != NULL) { 
14             n = strlen (line); 
15             if (write (fd1[1], line, n) != n)
16                 err_sys ("write error to pipe"); 
17             if ((n = read (fd2[0], line, MAXLINE)) < 0)
18                 err_sys ("read error from pipe"); 
19 
20             if (n == 0) { 
21                 err_msg ("child closed pipe"); 
22                 break;
23             }
24             line[n] = 0; 
25             if (fputs (line, stdout) == EOF)
26                 err_sys ("fputs error"); 
27         }
28 
29         if (ferror (stdin))
30             err_sys ("fputs error"); 
31 
32         return 0; 
33     }
34     else { 
35         close (fd1[1]); 
36         close (fd2[0]); 
37         if (fd1[0] != STDIN_FILENO) { 
38             if (dup2 (fd1[0], STDIN_FILENO) != STDIN_FILENO)
39                 err_sys ("dup2 error to stdin"); 
40             close (fd1[0]); 
41         }
42 
43         if (fd2[1] != STDOUT_FILENO) { 
44             if (dup2 (fd2[1], STDOUT_FILENO) != STDOUT_FILENO)
45                 err_sys ("dup2 error to stdout"); 
46             close (fd2[1]); 
47         }
48 
49         if (execl (argv[1], "add2", (char *)0) < 0)
50             err_sys ("execl error"); 
51     }

這個程序創建兩個管道,fd1用來寫請求,fd2用來讀應答;對子進程而言,fd1重定向到標準輸入,fd2重定向到標準輸出,讀取stdin中的數據相加然後寫入stdout完成工作。父進程在取得應答後向標準輸出寫入結果。

如果在Solaris上,可以直接用一個pipe同時讀寫,代碼可以重寫成這樣:

 1 int fd[2];
 2 if (pipe(fd) < 0) 
 3     err_sys("pipe error\n");
 4 
 5 char line[MAXLINE];
 6 pid_t pid = fork();
 7 if (pid < 0)
 8     err_sys("fork error\n");
 9 else if (pid > 0)
10 {
11     close(fd[1]);
12     while (fgets(line, MAXLINE, stdin) != NULL) {
13         n = strlen(line);
14         if (write(fd[0], line, n) != n)
15             err_sys("write error to pipe\n")
16         if ((n = read(fd[0], line, MAXLINE)) < 0) 
17             err_sys("read error from pipe\n");
18 
19         if (n == 0) 
20             err_sys("child closed pipe\n");
21         line[n] = 0;
22         if (fputs(line, stdout) == EOF) 
23             err_sys("fputs error\n");
24     }
25 
26     if (ferror(stdin))
27         err_sys("fputs error\n");
28 
29     return 0;
30 }
31 else {
32     close(fd[0]);
33     if (fd[1] != STDIN_FILENO)
34         if (dup2(fd[1], STDIN_FILENO) != STDIN_FILENO)
35             err_sys("dup2 error to stdin\n");
36 
37     if (fd[1] != STDOUT_FILENO) {
38         if (dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO)
39             err_sys("dup2 error to stdout\n");
40         close(fd[1]);
41     }
42 
43     if (execl(argv[1], argv[2], (char *)0) < 0)
44         err_sys("execl error\n");
45 
46 }

代碼清爽多了,不用去考慮fd1[0]和fd2[1]是啥意思是一件很養腦的事。

不過這樣的代碼只能在Solaris上運行(聽說BSD也支持?),如果考慮到可移植性,還是寫上面的比較穩妥。

 

測試程序

 

 

2. solaris pipe 可以脫離父子關係建立

pipe 好用但是沒法脫離fork使用,一般的pipe如果想讓任意兩個進程通訊,得藉助它的變身fifo來實現。

關於FIFO,詳情可參考我之前寫的一篇文章:

 

而Solaris上的pipe沒這麼多事,加入兩個調用:fattach / fdetach,你就可以像使用FIFO一樣使用pipe了:

 1 int fd[2];
 2 if (pipe(fd) < 0)
 3     err_sys("pipe error\n");
 4 
 5 if (fattach(fd[1], "./pipe") < 0)
 6     err_sys("fattach error\n");
 7 
 8 printf("attach to file pipe ok\n");
 9 
10 close(fd[1]);
11 char line[MAXLINE];
12 while (fgets(line, MAXLINE, stdin) != NULL) {
13     n = strlen(line);
14     if (write(fd[0], line, n) != n)
15         err_sys("write error to pipe\n");
16     if ((n = read(fd[0], line, MAXLINE)) < 0)
17         err_sys("read error from pipe\n");
18 
19     if (n == 0) 
20         err_sys("child closed pipe\n");
21 
22     line[n] = 0;
23     if (fputs(line, stdout) == EOF)
24         err_sys("fputs error\n");
25 }
26 
27 if (ferror(stdin))
28     err_sys("fputs error\n");
29 
30 if (fdetach("./pipe") < 0)
31     err_sys("fdetach error\n");
32 
33 printf("detach from file pipe ok\n");

在pipe調用之後立即加入fattach調用,可以將管道關聯到文件系統的一個文件名上,該文件必需事先存在,且可讀可寫。

在fattach調用之前這個文件(./pipe)是個普通文件,打開讀寫都是磁盤IO;

在fattach調用之後,這個文件就變身成為一個管道了,打開讀寫都是內存流操作,且管道的另一端就是attach的那個進程。

子進程也需要改造一下,以便使用pipe通訊:

 1 int fd, n, int1, int2;
 2 char line[MAXLINE];
 3 fd = open("./pipe", O_RDWR);
 4 if (fd < 0)
 5     err_sys("open file pipe failed\n");
 6 
 7 printf("open file pipe ok, fd = %d\n", fd);
 8 while ((n = read(fd, line, MAXLINE)) > 0) {
 9     line[n] = 0;
10     if (sscanf(line, "%d%d", &int1, &int2) == 2) {
11         sprintf(line, "%d\n", int1 + int2);
12         n = strlen(line);
13         if (write(fd, line, n) != n)
14             err_sys("write error\n");
15 
16         printf("i am working on %s\n", line);
17     }
18     else {
19         if (write(fd, "invalid args\n", 13) != 13)
20             err_sys("write msg error\n");
21     }
22 }
23 
24 close(fd);

打開pipe就如同打開普通文件一樣,open直接搞定。當然前提是attach進程必需已經在運行。

當attach進程detach后,管道文件又將恢復它的本來面目。

 

脫離了父子關係的pipe其實可以建立多對一關係(多對多關係不可以,因為只能有一個進程attach)。

例如開4個cmd窗口,分別執行以下命令:

./padd2 abc
./add2
./add2
./add2

 向attach進程(padd2)發送9個計算請求后,可以看到輸出結果如下:

-bash-3.2$ ./padd2 abc
attach to file pipe ok
1 1
2
2 2
4
3 3 
6
4 4
8
5 5
10
6 6 
12
7 7 
14
8 8
16
9 9
18

 再回來看各個open管道的進程,輸出分別如下:

-bash-3.2$ ./add2
open file pipe ok, fd = 3
source: 1 1
i am working on 2
source: 4 4
i am working on 8
source: 7 7 
i am working on 14 

 

-bash-3.2$ ./add2
open file pipe ok, fd = 3
source: 2 2
i am working on 4
source: 5 5
i am working on 10
source: 9 9
i am working on 18 

 

-bash-3.2$ ./add2
open file pipe ok, fd = 3
source: 2 2
i am working on 4
source: 5 5
i am working on 10
source: 9 9
i am working on 18 

 

-bash-3.2$ ./add2
open file pipe ok, fd = 3
source: 3 3
i am working on 6
source: 6 6
i am working on 12
source: 8 8 
i am working on 16

 

可以發現一個很有趣的現象,就是各個add2進程基本是輪着來獲取請求的,可以猜想底層的pipe可能有一個進程排隊機制。

但是反過來使用pipe就不行了。就是說當啟動一個add3(區別於上例的add2與padd2)作為fattach端打開pipe,啟動多個padd3作為open端使用pipe,

然後通過命令行給padd3傳遞要相加的值,可以寫一個腳本同時啟動多個padd3,來查看效果:

#! /bin/sh
./padd3 1 1 &
./padd3 2 2 &
./padd3 3 3 &
./padd3 4 4 &

 這個腳本中啟動了4個加法進程,同時向add3發送4個加法請求,腳本中四個進程輸出如下:

-bash-3.2$ ./padd3.sh
-bash-3.2$ open file pipe ok, fd = 3
1 1 = 2
open file pipe ok, fd = 3
2 2 = 4
open file pipe ok, fd = 3
open file pipe ok, fd = 3
4 4 = 37

 可以看到3+3的請求被忽略了,轉到add3查看輸出:

-bash-3.2$ ./add3
attach to file pipe ok
source: 1 1
i am working on 1 + 1 = 2
source: 2 2
i am working on 2 + 2 = 4
source: 3 34 4
i am working on 3 + 34 = 37

 原來是3+3與4+4兩個請求粘連了,導致add3識別成一個3+34的請求,所以出錯了。

多運行幾遍腳本后,發現還有這樣的輸出:

-bash-3.2$ ./padd3.sh
-bash-3.2$ open file pipe ok, fd = 3
4 4 = 2
open file pipe ok, fd = 3
2 2 = 4
open file pipe ok, fd = 3
3 3 = 6
open file pipe ok, fd = 3
1 1 = 8

  4+4=2?1+1=8?再看add3這頭的輸出:

-bash-3.2$ ./add3
attach to file pipe ok
source: 1 1
i am working on 1 + 1 = 2
source: 2 2
i am working on 2 + 2 = 4
source: 3 3
i am working on 3 + 3 = 6
source: 4 4
i am working on 4 + 4 = 8

 完全正常呢。

經過一番推理,發現是4+4的請求取得了1+1請求的應答;1+1的請求取得了4+4的應答。

可見這樣的結構還有一個弊端,同時請求的進程可能無法得到自己的應答,應答與請求之間相互錯位。

所以想用fattach來實現多路請求的人還是洗洗睡吧,畢竟它就是一個pipe不是,還能給它整成tcp么?

而之前的例子可以,是因為請求是順序發送的,上個請求得到應答后才發送下個請求,所以不存在這個例子的問題(但是實用性也不高)。

 

測試程序

 

 

3. solaris pipe 可以通過connld模塊實現類似tcp的多路連接

第2條剛說不能實現多路連接,第3條就接着來打臉了,這是由於Solaris上的pipe都是基於STREAMS技術構建,

而STREAMS是支持靈活的PUSH、POP流處理模塊的,再加上STREAMS傳遞文件fd的能力,就可以支持類似tcp中accept的能力。

即每個open pipe文件的進程,得到的不是原來管道的fd,而是新創建管道的fd,而管道的另一側fd則通過已有的管道發送到attach進程,

後者使用這個新的fd與客戶進程通訊。為了支持多路連接,我們的代碼需要重新整理一下,首先看客戶端:

1 int fd;
2 char line[MAXLINE];
3 fd = cli_conn("./pipe");
4 if (fd < 0)
5     return 0;

這裏將open相關邏輯封裝到了cli_conn函數中,以便之後復用:

 1 int cli_conn(const char *name)
 2 {
 3     int fd;
 4     if ((fd = open(name, O_RDWR)) < 0) {
 5         printf("open pipe file failed\n");
 6         return -1;
 7     }
 8 
 9     if (isastream(fd) == 0) {
10         close(fd);
11         return -2;
12     }
13 
14     return fd;
15 }

可以看到與之前幾乎沒有變化,只是增加了isastream調用防止attach進程沒有啟動。

再來看下服務端:

 1 int listenfd = serv_listen("./pipe");
 2 if (listenfd < 0)
 3     return 0;
 4 
 5 int acceptfd = 0;
 6 int n = 0, int1 = 0, int2 = 0;
 7 char line[MAXLINE];
 8 uid_t uid = 0;
 9 while ((acceptfd = serv_accept(listenfd, &uid)) >= 0)
10 {
11     printf("accept a client, fd = %d, uid = %ld\n", acceptfd, uid);
12     while ((n = read(acceptfd, line, MAXLINE)) > 0) {
13         line[n] = 0;
14         printf("source: %s\n", line);
15         if (sscanf(line, "%d%d", &int1, &int2) == 2) {
16             sprintf(line, "%d\n", int1 + int2);
17             n = strlen(line);
18             if (write(acceptfd, line, n) != n) {
19                 printf("write error\n");
20                 return 0;
21             }
22             printf("i am working on %d + %d = %s\n", int1, int2, line);
23         }
24         else {
25             if (write(acceptfd, "invalid args\n", 13) != 13) {
26                 printf("write msg error\n");
27                 return 0;
28             }
29         }
30     }
31 
32     close(acceptfd);
33 }
34 
35 if (fdetach("./pipe") < 0) {
36     printf("fdetach error\n");
37     return 0;
38 }
39 
40 printf("detach from file pipe ok\n");
41 close(listenfd);

首先調用serv_listen建立基本pipe,然後不斷在該pipe上調用serv_accept來獲取獨立的客戶端連接。之後的邏輯與以前一樣。

現在重點看下封裝的這兩個方法:

 1 int serv_listen(const char *name)
 2 {
 3     int tempfd;
 4     int fd[2];
 5     unlink(name);
 6     tempfd = creat(name, FIFO_MODE);
 7     if (tempfd < 0) {
 8         printf("creat failed\n");
 9         return -1;
10     }
11 
12     if (close(tempfd) < 0) {
13         printf("close temp fd failed\n");
14         return -2;
15     }
16 
17     if (pipe(fd) < 0) {
18         printf("pipe error\n");
19         return -3;
20     }
21 
22     if (ioctl(fd[1], I_PUSH, "connld") < 0) {
23         printf("I_PUSH connld failed\n");
24         close(fd[0]);
25         close(fd[1]);
26         return -4;
27     }
28 
29     printf("push connld ok\n");
30     if (fattach(fd[1], name) < 0) {
31         printf("fattach error\n");
32         close(fd[0]);
33         close(fd[1]);
34         return -5;
35     }
36 
37     printf("attach to file pipe ok\n");
38     close(fd[1]);
39     return fd[0];
40 }

serv_listen封裝了與建立基本pipe相關的代碼,首先確保pipe文件存在且可讀寫,然後創建普通的pipe,在fattach調用之前必需先PUSH一個connld模塊到該pipe STREAM中。這樣就大功告成!

 1 int serv_accept(int listenfd, uid_t *uidptr)
 2 {
 3     struct strrecvfd recvfd;
 4     if (ioctl(listenfd, I_RECVFD, &recvfd) < 0) {
 5         printf("I_RECVFD from listen fd failed\n");
 6         return -1;
 7     }
 8 
 9     if (uidptr)
10         *uidptr = recvfd.uid;
11 
12     return recvfd.fd;
13 }

當有客戶端連接上來的時候,使用I_RECVFD接收connld返回的另一個pipe的fd。之後的數據將在該pipe進行。

看了看,感覺和tcp的listen與accept別無二致,看來天下武功,至精深處都是英雄所見略同。

之前的多個客戶端同時運行的例子再跑一遍,觀察attach端輸出:

-bash-3.2$ ./add4
push connld ok
attach to file pipe ok
accept a client, fd = 4, uid = 101
source: 1 1
i am working on 1 + 1 = 2
accept a client, fd = 4, uid = 101
source: 2 2
i am working on 2 + 2 = 4
accept a client, fd = 4, uid = 101
source: 3 3
i am working on 3 + 3 = 6
accept a client, fd = 4, uid = 101
source: 4 4
i am working on 4 + 4 = 8

 一切正常。再看下腳本中四個進程的輸出:

-bash-3.2$ ./padd4.sh
-bash-3.2$ open file pipe ok, fd = 3
1 1 = 2
open file pipe ok, fd = 3
2 2 = 4
open file pipe ok, fd = 3
3 3 = 6
open file pipe ok, fd = 3
4 4 = 8

 也是沒問題的,既沒有出現多個請求粘連的情況,也沒有出現請求與應答錯位的情況。

 

測試程序

 

 

4.結論

Solaris 上的pipe不僅可以全雙工通訊、不依賴父子進程關係,還可以實現類似tcp那樣分離多個客戶端通訊連接的能力。

雖然Solaris前途未卜,但是希望一些好的東西還是能流傳下來,就比如這個神奇的pipe。

 

看完今天的文章,你是不是對特立獨行的Solaris又加深了一層了解?歡迎留言區說說你認識的Solaris。

 

 

 

 

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

eNSP仿真軟件之利用單臂路由實現VLAN間路由

1、 實驗原理

以太網中,通常會使用VLAN技術隔離二層廣播域來減少廣播的影響,並增強網絡的安全性和可管理性。其缺點是同時也嚴格地隔離了不同VLAN之間的任何二層流量,使分屬於不同VLAN的用戶不能直接互相通信。在現實中,經常會出現某些用戶需要跨越VLAN實現通信的情況,單臂路由技術就是解決VLAN間通信的一種方法。

單臂路由的原理是通過一台路由器, 使VLAN間互通數據通過路由器進行三層轉發。如果在路由器上為每個VLAN分配一個單獨的路由器物理接口,隨着VLAN數量的增加,必然需要更多的接口,而路由器能提供的接口數量比較有限,所以在路由器的一個物理接口上通過配置子接口(即邏輯接口)的方式來實現以一當多的功能,將是一種非常好的方式。路由器同一物理接口的不同子接口作為不同VLAN的默認網關,當不同VLAN間的用戶主機需要通信時,只需將數據包發送給網關,網關處理后再發送至目的主機所在VLAN,從而實現VLAN間通信。由於從拓撲結構圖上看,在交換機與路由器之間,數據僅通過一條物理鏈路傳輸,故被形象地稱之為“單臂路由”。

2、 實驗內容

本實驗模擬公司網絡場景。路由器R1是公司的出口網關,員工PC通過接入層交換機(如S2和S3)接入公司網絡,接入層交換機又通過匯聚交換機S1與路由器R1相連。公司內部網絡通過劃分不同的VLAN隔離了不同部門之間的二層通信,保證各部門間的信息安全,但是由於業務需要,經理、市場部和人事部之間需要能實現跨VLAN通信,網絡管理員決定藉助路由器的三層功能,通過配置單臂路由來實現。

3、 實驗步驟

(1)、新建實驗拓補圖

 

(2)根據實驗編址表進行路由器R1和PC1-3的IP地址,其中路由器的配置方式如下:

配置路由器子接口和IP地址:

★在R1上創建子接口GE 0/0/1.1,配置IP地址為192.168.1.254/24,作為人事部網關地址。

★同理創建子接口並且配置IP地址

(3)公司為保障各部門的信息安全,需保證隔離不同部門間的二層通信,規劃各部門的終端屬於不同的VLAN,併為PC配置相應IP地址。

★在S2上創建VLAN 10和VLAN20,把連接PC-1的E 0/0/1和連接PC-2的E 0/0/2接口配置為Access類型接口,並分別劃分到相應的VLAN中。

★交換機之間或交換機和路由器之間相連的接口需要傳遞多個VLAN信息,需要配置成Trunk接口。將S2和S3的GE 0/0/2接口配置成Trunk類型接口,並允許所有VLAN通過 

 

 

 

★在S1上創建VLAN10、VLAN20和VLAN30,並配置交換機和路由器相連的接口為Trunk,允許所有VLAN通過。

(4)測試PC1-3的連通性,發現仍然不能聯通。

(5)配置路由器子接口封裝VLAN

雖然目前已經創建了不同的子接口,並配置了相關IP地址,但是仍然無法通信。這是由於處於不同VLAN下,不同網段的PC間要實現互相通信,數據包必須通過路由器進行中轉。由S1發送到RI的數據都加上了VLAN標籤,而路由器作為三層設備,默認無法處理帶了VLAN標籤的數據包。因此需要在路由器上的子接口下配置對應VLAN的封裝,使路由器能夠識別和處理VLAN標籤,包括剝離和封裝VLAN標籤。

★在R1的子接口GE 0/0/1.1.上封裝VLAN 10,在子接口GE 0/0/1.2上封裝VLAN 20。在子接口GE 0/0/1.3上封裝VLAN30,並開啟子接口的ARP廣播功能。

使用dot1q termination vid命令配置子接口對一層tag報文的終結功能。即配置該命令后,路由器子接口在接收帶有VLAN tag的報文時,將剝掉tag進行三層轉發,在發送報文時,會將與該子接口對應VLAN的VLAN tag添加到報文中。

使用arp broadcast enable命令開啟子接口的ARP廣播功能。如果不配置該命令,將會導致該子接口無法主動發送ARP廣播報文,以及向外轉發IP報文。 

同理配置R1的子接口GE 0/0/1.2和GE 0/0/1.3。

(7)      配置完成后,在路由器R1上查看接口狀態,可以看到3個子接口的物理狀態和協議狀態都正常。

(8)      查看路由器R1的路由表,可以觀察到,路由表中已經有了192.168.1.0/24、 192.168.2.0/24、 192. 168.3.0/24的路由條目,並且都是路由器R1的直連路由,類似於路由器上的直連物理接口 。

(9)      測試連通性。可以看到PC1和PC2已經可以PING通

(10)      在PC-1上tracertPC-2,可以觀察到PC-1先把ping包發送給自身的網關192.168.1.254, 然後再由網關發送到PC-2。

現以PC-1pingPC-2為例,分析單臂路由的整個運作過程。

      兩台PC由於處於不同的網絡中,這時PC-1會將數據包發往自己的網關,即路由器R1的子接口GE 0/0/1.1的地址192.168.1.254。.

      數據包到達路由器R1后,由於路由器的子接口GE 0/0/1.1已經配置了VLAN封裝,當接收到PC-1發送的VLAN 10的數據幀時,發現數據幀的VLANID跟自身GE0/0/1.1接口配置的VLAN ID 一樣,便會剝離掉數據幀的VLAN標籤后通過三層路由轉發。

      通過查找路由表后,發現數據包中的目的地址192.168.2.1所屬的192.168.2.0/24 網段的路由條目,已經是路由器R1上的直連路由,且出接口為GE 0/0/1.2,便將該數據包發送至GE 0/0/1.2接口。

      當GE0/0/1.2接口接收到一個沒有帶VLAN標籤的數據幀時,便會加上自身接口所配置的VLAN ID 20后再進行轉發,然後通過交換機將數據幀順利轉發給PC-2。

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

[ch02-03] 梯度下降

系列博客,原文在筆者所維護的github上:,
點擊star加星不要吝嗇,星越多筆者越努力。

2.3 梯度下降

2.3.1 從自然現象中理解梯度下降

在大多數文章中,都以“一個人被困在山上,需要迅速下到谷底”來舉例,這個人會“尋找當前所處位置最陡峭的地方向下走”。這個例子中忽略了安全因素,這個人不可能沿着最陡峭的方向走,要考慮坡度。

在自然界中,梯度下降的最好例子,就是泉水下山的過程:

  1. 水受重力影響,會在當前位置,沿着最陡峭的方向流動,有時會形成瀑布(梯度下降);
  2. 水流下山的路徑不是唯一的,在同一個地點,有可能有多個位置具有同樣的陡峭程度,而造成了分流(可以得到多個解);
  3. 遇到坑窪地區,有可能形成湖泊,而終止下山過程(不能得到全局最優解,而是局部最優解)。

2.3.2 梯度下降的數學理解

梯度下降的數學公式:

\[\theta_{n+1} = \theta_{n} – \eta \cdot \nabla J(\theta) \tag{1}\]

其中:

  • \(\theta_{n+1}\):下一個值;
  • \(\theta_n\):當前值;
  • \(-\):減號,梯度的反向;
  • \(\eta\):學習率或步長,控制每一步走的距離,不要太快以免錯過了最佳景點,不要太慢以免時間太長;
  • \(\nabla\):梯度,函數當前位置的最快上升點;
  • \(J(\theta)\):函數。

梯度下降的三要素

  1. 當前點;
  2. 方向;
  3. 步長。

為什麼說是“梯度下降”?

“梯度下降”包含了兩層含義:

  1. 梯度:函數當前位置的最快上升點;
  2. 下降:與導數相反的方向,用數學語言描述就是那個減號。

亦即與上升相反的方向運動,就是下降。

圖2-9 梯度下降的步驟

圖2-9解釋了在函數極值點的兩側做梯度下降的計算過程,梯度下降的目的就是使得x值向極值點逼近。

2.3.3 單變量函數的梯度下降

假設一個單變量函數:

\[J(x) = x ^2\]

我們的目的是找到該函數的最小值,於是計算其微分:

\[J'(x) = 2x\]

假設初始位置為:

\[x_0=1.2\]

假設學習率:

\[\eta = 0.3\]

根據公式(1),迭代公式:

\[x_{n+1} = x_{n} – \eta \cdot \nabla J(x)= x_{n} – \eta \cdot 2x\tag{1}\]

假設終止條件為J(x)<1e-2,迭代過程是:

x=0.480000, y=0.230400
x=0.192000, y=0.036864
x=0.076800, y=0.005898
x=0.030720, y=0.000944

上面的過程如圖2-10所示。

圖2-10 使用梯度下降法迭代的過程

2.3.4 雙變量的梯度下降

假設一個雙變量函數:

\[J(x,y) = x^2 + \sin^2(y)\]

我們的目的是找到該函數的最小值,於是計算其微分:

\[{\partial{J(x,y)} \over \partial{x}} = 2x\]
\[{\partial{J(x,y)} \over \partial{y}} = 2 \sin y \cos y\]

假設初始位置為:

\[(x_0,y_0)=(3,1)\]

假設學習率:

\[\eta = 0.1\]

根據公式(1),迭代過程是的計算公式:
\[(x_{n+1},y_{n+1}) = (x_n,y_n) – \eta \cdot \nabla J(x,y)\]
\[ = (x_n,y_n) – \eta \cdot (2x,2 \cdot \sin y \cdot \cos y) \tag{1}\]

根據公式(1),假設終止條件為\(J(x,y)<1e-2\),迭代過程如表2-3所示。

表2-3 雙變量梯度下降的迭代過程

迭代次數 x y J(x,y)
1 3 1 9.708073
2 2.4 0.909070 6.382415
15 0.105553 0.063481 0.015166
16 0.084442 0.050819 0.009711

迭代16次后,J(x,y)的值為0.009711,滿足小於1e-2的條件,停止迭代。

上面的過程如表2-4所示,由於是雙變量,所以需要用三維圖來解釋。請注意看兩張圖中間那條隱隱的黑色線,表示梯度下降的過程,從紅色的高地一直沿着坡度向下走,直到藍色的窪地。

表2-4 在三維空間內的梯度下降過程

觀察角度1 觀察角度2

2.3.5 學習率η的選擇

在公式表達時,學習率被表示為\(\eta\)。在代碼里,我們把學習率定義為learning_rate,或者eta。針對上面的例子,試驗不同的學習率對迭代情況的影響,如表2-5所示。

表2-5 不同學習率對迭代情況的影響

學習率 迭代路線圖 說明
1.0 學習率太大,迭代的情況很糟糕,在一條水平線上跳來跳去,永遠也不能下降。
0.8 學習率大,會有這種左右跳躍的情況發生,這不利於神經網絡的訓練。
0.4 學習率合適,損失值會從單側下降,4步以後基本接近了理想值。
0.1 學習率較小,損失值會從單側下降,但下降速度非常慢,10步了還沒有到達理想狀態。

代碼位置

ch02, Level3, Level4, Level5

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

ASP.NET Core gRPC 使用 Consul 服務註冊發現

一. 前言

gRPC 在當前最常見的應用就是在微服務場景中,所以不可避免的會有服務註冊與發現問題,我們使用gRPC實現的服務可以使用 Consul 或者 etcd 作為服務註冊與發現中心,本文主要介紹Consul。

二. Consul 介紹

Consul是一種服務網絡解決方案,可跨任何運行平台以及公共或私有雲來連接和保護服務。它可以讓你發現服務並保護網絡流量。它可以在Kubernetes中使用,實現服務發現和服務網格功能(k8s默認etcd)。 提供安全服務通訊,保護和觀察服務之間的通信,而無需修改其代碼。 提供動態負載平衡, 使用Consul和HAProxy,Nginx或F5自動執行負載均衡器配置。 Consul 可以用於服務發現和服務網格。

翻譯自官網

三. Consul 安裝配置

安裝

Consul 下載地址:

根據自己的系統來選擇,我這裏選擇的是 Windows 版本的,直接解壓即可運行。

啟動

consul agent -dev -ui

本文不詳細介紹Consul使用,如需請自行查看相關資料

四. .NET Core Consul 客戶端的選擇

Consul 提供了 HTTP API 的方式來進行通訊,我們可以直接調用API或者是使用第三方封裝好的客戶端組件,通過Nuget搜索可以發現許多。

這裏面我沒有一一測試,但是目前使用量最多的 Consul 組件是不支持設置 GRPC 健康檢查的,而且 github 也停止了更新。

所以我 Fork 了這個倉庫,然後添加了 GRPC 的健康檢查支持,本文也將使用這個庫,歡迎大家使用:

因為原倉庫已經 Archived 了,所以我才 Fork 了自己改一下,改動很小,不影響原來的穩定性。

Nuget:

Github:

求個star

關於支持 GPRC 健康檢查的好處:

偷個懶,不翻譯了,摘自GRPC官方文檔

五. 註冊GRPC服務與健康檢查

基於前文()的Demo

1.為服務端項目安裝 NConsul.AspNetCore ( )

這裏面對 AspNetCore 做了適配,使用簡單。

2.在 Startup 的 ConfigureServices方法內進行配置

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc();

    services.AddConsul("http://localhost:8500")
        .AddGRPCHealthCheck("localhost:5000")
        .RegisterService("grpctest","localhost",5000,new []{"xc/grpc/test"});
}

AddConsul 添加 Consul Server 地址。

AddGRPCHealthCheck 添加 GRPC 健康檢查,即健康檢查走的是 GRPC 協議,該值為 GRPC 服務的地址,不需要path不需要提供 http/https

RegisterService 註冊服務

到這步,還不能啟動運行,如果運行健康檢查是會失敗的。

3.編寫 Health Check 服務 **

對於 GRPC 的健康檢查,官方有標準的定義,新建一個 proto 文件,命名為 HealthCheck.proto

syntax = "proto3";

package grpc.health.v1;

message HealthCheckRequest {
    string service = 1;
}

message HealthCheckResponse {
    enum ServingStatus {
        UNKNOWN = 0;
        SERVING = 1;
        NOT_SERVING = 2;
    }
    ServingStatus status = 1;
}

service Health {
    rpc Check(HealthCheckRequest) returns (HealthCheckResponse);

    rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}

這裏面的內容不得更改,是官方標準,資料見後文

這裏編譯一下項目,以便自動生成代碼。

然後,添加一個服務的實現類 HealthCheckService

public class HealthCheckService:Health.HealthBase
{
    public override Task<HealthCheckResponse> Check(HealthCheckRequest request, ServerCallContext context)
    {
        //TODO:檢查邏輯
        return Task.FromResult(new HealthCheckResponse(){Status = HealthCheckResponse.Types.ServingStatus.Serving});
    }

    public override async Task Watch(HealthCheckRequest request, IServerStreamWriter<HealthCheckResponse> responseStream, ServerCallContext context)
    {
        //TODO:檢查邏輯
        await responseStream.WriteAsync(new HealthCheckResponse()
            {Status = HealthCheckResponse.Types.ServingStatus.Serving});
    }
}

示例代碼直接返回了檢查結果,實際使用中應該在這裏編寫檢查邏輯,然後根據情況返回相應的檢查結果。檢查結果有3種情況:

結果類型 說明
Unknown 未知狀態
Serving 正常
NotServing 異常,不能提供服務

最後別忘了註冊服務:

4.測試運行

啟動 GRPC 服務

然後訪問 http://localhost:8500/ui 訪問 Consul 控制台

可以看到服務成功註冊,並且健康檢查也是通過了的。通過控制台日誌,還可以看到健康檢查的請求:

六. 客戶端使用服務發現

客戶端項目安裝 Consul 組件,然後改造下代碼:

static async Task Main(string[] args)
{
    var serviceName = "grpctest";
    var consulClient = new ConsulClient(c => c.Address = new Uri("http://localhost:8500"));
    var services = await consulClient.Catalog.Service(serviceName);
    if (services.Response.Length == 0)
    {
        throw new Exception($"未發現服務 {serviceName}");
    }

    var service = services.Response[0];
    var address = $"http://{service.ServiceAddress}:{service.ServicePort}";

    Console.WriteLine($"獲取服務地址成功:{address}");

    //啟用通過http使用http2.0
    AppContext.SetSwitch(
        "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
    var channel = GrpcChannel.ForAddress(address);
    var catClient = new LuCat.LuCatClient(channel);
    var catReply = await catClient.SuckingCatAsync(new Empty());
    Console.WriteLine("調用擼貓服務:"+ catReply.Message);
    Console.ReadKey();
}

通過服務名稱獲取服務地址,然後來進行訪問。

運行測試:

可以看到,成功的從Consul獲取了我們的服務地址,然後調用。

六. 參考資料

  • gRPC in Asp.Net Core :

  • GPRC Health Check Doc:

  • 本文 Demo:

  • 本系列文章目錄:

  • NConsul:

  • by Edison Zhou

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

MySQL權限管理

目錄

設置用戶與權限

一個MySQL系統可能有許多用戶。為了安全起見,root用戶通常只用管理目的。對於每個需要使用該系統的用戶,應該為他們創建一個賬號和密碼。這些用戶名和密碼不必與MySQL之外的用戶名稱和密碼(例如,Linux或NT用戶名和密碼)相同。同樣的原則也適合於root用戶。對於系統用戶和MySQL用戶最好使用不同的密碼,這一點對root用戶尤其應該這樣。
為用戶設置密碼不是必須的,但是強烈建議為所有創建的用戶設定密碼。要建立一個Web數據庫,最好為每個網站應用程序建立一個用戶。你可能會問,“為什麼要這麼做呢?”—-答案在於權限。

用戶管理

我們的MySQL的用戶,都被記錄在了mysql數據庫的user表中,如下圖:

在MySQL數據庫中,一個完整的用戶賬號應該包含用戶名登錄主機,也就是說 用戶名@主機名才是一個完整的用戶賬號。

創建用戶基本語法:

CREATE USER 用戶名@主機名 IDENTIFIED BY '密碼';

刪除用戶基本語法:

DROP USER 用戶名@主機名;

修改用戶密碼:

- 修改自己的密碼
set password = password('新密碼');
- 修改別人的密碼(需要有該權限)
set password for 用戶名@主機名 = password('新密碼');

MySQL權限系統介紹

MySQL的最好特性之一是支持複雜的權限系統權限是對特定對象執行特定操作的權力,它與特定用戶相關。其概念非常類似於文件的權限。擋在MySQL中創建一個用戶時,就賦予了該用戶一定的權限,這些權限指定了用戶在本系統中可以做什麼和不可以做什麼。

最少權限原則

最少權限原則可以用來提高任何計算機系統的安全性。它是一個基本的、但又是非常重要的而且容易為我們忽略的原則。該原則包含如下內容:

一個用戶(或一個進程)應該擁有能夠執行分配給他的任務的最低級別的權限。

該原則同樣適用於MySQL,就像它應用於其他地方一樣。例如,要在網站上運行查詢,用戶並不需要root用戶所擁有的所有權限。因此,我們應該創建另一個用戶,這個用戶只有訪問我們剛剛建立的數據庫的必要權限。

創建用戶:GRANT命令

GRANT和REVOKE命令分別用來授予取消MySQL用戶的權限,這些權限分4個級別。它們分別是:

  • 全局
  • 數據庫

GRANT命令用來創建用戶並賦予他們權限。GRANT命令的常見形式是:

GRANT privileges [columns]
ON item
TO user_name [IDENTIFIED BY 'password']
[REQUIRE ssl_options]
[WITH [GRANT OPTION | limit_options]]

普通寫法可以簡略如下:

GRANT 權限列表 ON 數據庫.表 TO 用戶名@主機名 [IDENTIFIED BY '密碼'];

identified by可以省略,也可以寫出
(1)如果寫了,用戶存在,就是修改用戶的密碼
(2)如果寫了,該用戶不存在,就是創建用戶,同時指定密碼

方括號內的子句是可選的。在本語法中,出現了許多佔位符。第一個佔位符是 privileges,應該是由逗號分開的一組權限。MySQL已經有一組已定義的權限。它們在下一節詳細介紹。
佔位符 columns是可選的。可以用它對每一個列指定權限。也可以使用單列的名稱或者逗號分開的一組列的名稱。

佔位符 item是新權限的所應用於的數據庫或表。可以將項目指定為“.”,而將權限應用於所有數據庫。這叫做賦予全局權限。如果沒有使用在特定的數據庫,也可以通過只指定*完成賦予全局權限。更常見的是,以dbname.*的形式指定數據庫中所有的表,以dbname.tablename的形式指定單個表,或者通過指定tablename來指定特定的列。這些分別表示其他3個可以利用的權限:數據庫、表、列。如果在輸入命令的時候正在使用一個數據庫,tablename本身將被解釋成當前數據庫中的一個表。

user_name應該是用戶登錄MySQL所使用的用戶名。請注意,它不必與登錄系統時所使用的用戶名相同。MySQL中的user_name也可以包含一個主機名。可以用它來區分如itbsl(解釋成itbsl@localhost)和itbsl@somewhere.com。這是非常有用的一項能力,因為來自不同域的用戶經常可能使用同一個名字。這也提高了安全性能,因為可以指定用戶從什麼地方連接到本機,甚至可以指定它們在特定的地方可以訪問那些表和數據庫。

password應該是用戶登錄時使用的密碼。常見的密碼選擇規則在這裏都適用。我們後面將更詳細地講述安全問題,但是密碼應該不容易被猜出來。這意味着,密碼不應該是一個字段單詞或與用戶名相同。理想的密碼應該是大、小寫字母和非字母的組合。

REQUIRE子句允許指定用戶是否必須通過加密套接字連接,或者指定其它的SSL選項,關於SSL到MySQL連接的更多信息,請參閱MySQL手冊。

WITH GRANT OPTION選項,如果指定,表示允許指定的用戶向別人授予自己所擁有的權限。
我們也可以指定如下所示的WITH子句:
MAX_QUERIES_PER_HOUR n
或者
MAX_UPDATES_PER_HOUR n
或者
MAX_CONNECTIONS_PER_HOUR n
這些子句可以指定每個用戶每小時執行的查詢、更新和鏈接的數量。在共享的系統上限制單個用戶的負載時,這些子句是非常有用的。
權限存儲在名為mysql的數據庫中的5個系統中。這些表分別是mysql.user、mysql.db、mysql.host、mysql.tables_priv和mysql.columns_priv。作為GRANT命令的替代,可以直接修改這些表。

權限的類型和級別

MySQL中存在3個基本類型的權限:適用於賦予一般用戶的權限、適用於賦予管理員的權限和幾個特定的權限。任何用戶都可以被賦予這3類權限,但是根據最少權限原則,最好嚴格限定只將管理員類型的權限賦予管理員。

我們應該只賦予用戶訪問他們必須使用的數據庫和表的權限。而不應該將訪問mysql的權限賦予不是管理員的人。mysql數據庫是所有用戶名、密碼等信息存儲的地方。

常規用戶的權限直接與特定的SQL命令類型以及用戶是否被允許運行它們相關。下錶所示的是基本用戶權限。“應用於”列下面的對象給出了該類型權限可以授予的對象。

用戶的權限

權限 應用於 描述
SELECT 表、列 允許用戶從表中查詢行(記錄)
INSERT 表、列 允許用戶在表中插入新行
UPDATE 表、列 允許用戶修改現存表裡行中的值
DELETE 允許用戶刪除現存表的行
INDEX 允許用戶創建和拖動特定表索引
ALTER 允許用戶改變現存表的結構,例如,可添加列、重命名列或表、修改列的數據類型
CREATE 數據庫、表 允許用戶創建新數據庫或表。如果在GRANT中指定了一個特定的數據庫或表,它們只能夠創建該數據庫或表,即它們必須首先刪除(drop)它
DROP 數據庫、表 允許用戶拖動(刪除)數據庫或表

從系統的安全性方面考慮,適於常規用戶的權限大多數是相對無害的。ALTER權限通過重命名表可能會影響權限系統,但是大多數用戶需要它。安全性常常是可用性與保險性的折中。遇到ALTER的時候,應當做出自己的選擇,但是通常還是會將這個權限授予用戶。

下面的表給出了適用於管理員用戶使用的權限。
可以將這些權限授予非管理員用戶,這樣做的時候要非常小心。
FILE權限有些不同,它對普通用戶非常有用,因為它可以將數據從文件載入數據庫,從而可以節省許多時間,否則,每次將數據輸入數據庫都需要重新輸入,這很浪費時間。
然而,文件載入可以用來載入MySQL可識別的任何文件,包括屬於其他用戶的數據庫和潛在的密碼文件。授予該權限的時候需要小心,或者自己為用戶載入數據。

管理員權限

權限 描述
CREATE TEMPORARY TABLES 允許管理員在CREATE TABLE語句中使用TEMPORARY關鍵字
FILE 允許將數據從文件讀入表,或從表讀入文件
LOCK TABLES 允許使用LOCK TABLES語句
PROCESS 允許管理員查看屬於所有用戶的服務器進程
RELOAD 允許管理員重新載入授權表、清空授權、主機、日誌和表
REPLICATION CLIENT 允許管理員重新載入授權表、和從機(Slave)上使用SHOW STATUS
REPLICATION SLAVE 允許複製從服務器連接到主服務器
SHOW DATABASES 允許使用SHOW DATABASES語句查看所有數據庫列表。沒有這個權限,用戶只能看到他們能夠看到的數據庫
SHUTDOWN 允許管理員關閉MySQL服務器
SUPER 允許管理員關閉屬於任何用戶的的線程

特別的權限

權限 描述
ALL 授予上面兩個表列表的所有權限。也可以將ALL寫成ALL PRIVILEGES
USAGE 不授予權限。這創建一個用戶並允許他登錄,但是不允許進行任何操作。通常會在以後授予該用戶更多的權限

REVOKE命令

與GRANT相反的命令是REVOKE。它用來從一個用戶收回權限。在語法上與GRANT非常相似:

REVOKE privileges [(columns)]
ON item
FROM user_name

中文翻譯:

REVOKE 權限列表 ON 數據庫.對象 FROM 用戶名@主機名;

如果已經給出了WITH GRANT OPTION子句,可以按如下方式撤銷它(以及所有其他權限):

REVOKE ALL PRIVILEGES, GRANT
FROM user_name

權限立即生效

當我們修改用戶權限之後,如果想不重啟立即生效,需要執行以下flush privileges,這樣能快速刷新權限

FULSH PRIVILEGES;

使用GRANT和REVOKE的例子

要創建一個管理員,可以輸入如下所示的命令:

grant all on * to fred identified by 'mnb123' with grant option;

以上命令授予了用戶名為fred、密碼為mnb123的用戶使用所有數據庫的所有權限,並允許他向其他人授予這些權限。

如果不希望用戶在系統中存在,可以按如下方式撤銷:

revoke all privileges, grant from red;

現在,我們可以按如下方式創建一個沒有任何權限的常規用戶:

grant usage on books.* to sally identified by 'magic123';

在與sally交談后,我們對她需要進行的操作有了一定的了解,因此按如下方式可以授予她適當的權限:

grant select, insert, update, delete, index, alter, create, drop on books.* to sally;

請注意,要完成這些,並不需要指定sally密碼。

如果我們認為sally權限過高,可能會決定按如下方式減少一些權限:

revoke alter, create, drop on books.* from sally;

後來,當她不再需要使用數據庫時,可以按如下方式撤銷所有的權限:

revoke all on books.* from sally;

參考資料:

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

18個awk的經典實戰案例

介紹

這些案例是我收集起來的,大多都是我自己遇到過的,有些比較經典,有些比較具有代表性。

這些awk案例我也錄了相關視頻的講解,歡迎大家去瞅瞅。

插入幾個新字段

在”abc d”的b後面插入3個字段e f g

echo a b c d|awk '{$3="e f g "$3}1'

格式化空白

移除每行的前綴、後綴空白,並將各部分左對齊。

      aaaa        bbb     ccc                 
   bbb     aaa ccc
ddd       fff             eee gg hh ii jj
awk 'BEGIN{OFS="\t"}{$1=$1;print}' a.txt

執行結果:

aaaa    bbb     ccc
bbb     aaa     ccc
ddd     fff     eee     gg      hh      ii      jj

篩選IPv4地址

從ifconfig命令的結果中篩選出除了lo網卡外的所有IPv4地址。

讀取.ini配置文件中的某段

[base]
name=os_repo
baseurl=https://xxx/centos/$releasever/os/$basearch
gpgcheck=0

enable=1

[mysql]
name=mysql_repo
baseurl=https://xxx/mysql-repo/yum/mysql-5.7-community/el/$releasever/$basearch

gpgcheck=0
enable=1

[epel]
name=epel_repo
baseurl=https://xxx/epel/$releasever/$basearch
gpgcheck=0
enable=1
[percona]
name=percona_repo
baseurl = https://xxx/percona/release/$releasever/RPMS/$basearch
enabled = 1
gpgcheck = 0

根據某字段去重

去掉uid=xxx重複的行。

2019-01-13_12:00_index?uid=123
2019-01-13_13:00_index?uid=123
2019-01-13_14:00_index?uid=333
2019-01-13_15:00_index?uid=9710
2019-01-14_12:00_index?uid=123
2019-01-14_13:00_index?uid=123
2019-01-15_14:00_index?uid=333
2019-01-16_15:00_index?uid=9710
awk -F"?" '!arr[$2]++{print}' a.txt

結果:

2019-01-13_12:00_index?uid=123
2019-01-13_14:00_index?uid=333
2019-01-13_15:00_index?uid=9710

次數統計

portmapper
portmapper
portmapper
portmapper
portmapper
portmapper
status
status
mountd
mountd
mountd
mountd
mountd
mountd
nfs
nfs
nfs_acl
nfs
nfs
nfs_acl
nlockmgr
nlockmgr
nlockmgr
nlockmgr
nlockmgr
awk '{arr[$1]++}END{OFS="\t";for(idx in arr){printf arr[idx],idx}}' a.txt

統計TCP連接狀態數量

$ netstat -tnap
Proto Recv-Q Send-Q Local Address   Foreign Address  State       PID/Program name
tcp        0      0 0.0.0.0:22      0.0.0.0:*        LISTEN      1139/sshd
tcp        0      0 127.0.0.1:25    0.0.0.0:*        LISTEN      2285/master
tcp        0     96 192.168.2.17:22 192.168.2.1:2468 ESTABLISHED 87463/sshd: root@pt
tcp        0      0 192.168.2017:22 192.168.201:5821 ESTABLISHED 89359/sshd: root@no
tcp6       0      0 :::3306         :::*             LISTEN      2289/mysqld
tcp6       0      0 :::22           :::*             LISTEN      1139/sshd
tcp6       0      0 ::1:25          :::*             LISTEN      2285/master

統計得到的結果:

5: LISTEN
2: ESTABLISHED

一行式:

netstat -tna | awk '/^tcp/{arr[$6]++}END{for(state in arr){print arr[state] ": " state}}'
netstat -tna | /usr/bin/grep 'tcp' | awk '{print $6}' | sort | uniq -c

統計日誌中各IP訪問非200狀態碼的次數

日誌示例數據:

111.202.100.141 - - [2019-11-07T03:11:02+08:00] "GET /robots.txt HTTP/1.1" 301 169 

統計非200狀態碼的IP,並取次數最多的前10個IP。

# 法一
awk '$8!=200{arr[$1]++}END{for(i in arr){print arr[i],i}}' access.log | sort -k1nr | head -n 10

# 法二:
awk '
    $8!=200{arr[$1]++}
    END{
        PROCINFO["sorted_in"]="@val_num_desc";
        for(i in arr){
            if(cnt++==10){exit}
            print arr[i],i
        }
}' access.log

統計獨立IP

​ url 訪問IP 訪問時間訪問人

a.com.cn|202.109.134.23|2015-11-20 20:34:43|guest
b.com.cn|202.109.134.23|2015-11-20 20:34:48|guest
c.com.cn|202.109.134.24|2015-11-20 20:34:48|guest
a.com.cn|202.109.134.23|2015-11-20 20:34:43|guest
a.com.cn|202.109.134.24|2015-11-20 20:34:43|guest
b.com.cn|202.109.134.25|2015-11-20 20:34:48|guest

需求:統計每個URL的獨立訪問IP有多少個(去重),並且要為每個URL保存一個對應的文件,得到的結果類似:

a.com.cn  2
b.com.cn  2
c.com.cn  1

並且有三個對應的文件:

a.com.cn.txt
b.com.cn.txt
c.com.cn.txt

代碼:

處理字段缺失的數據

ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
3   Tony    male    21                  17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18   ccc@xyz.com    18185904230
6   Andy    female       ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   bax@qq.com     17729348758
9   Steven          23   bc@sohu.com    15947893212
10  Bruce   female  27   bcbd@139.com   13942943905

當字段缺失時,直接使用FS劃分字段來處理會非常棘手。gawk為了解決這種特殊需求,提供了FIELDWIDTHS變量。

FIELDWIDTH可以按照字符數量劃分字段。

awk '{print $4}' FIELDWIDTHS="2 2:6 2:6 2:3 2:13 2:11" a.txt

處理字段中包含了字段分隔符的數據

下面是CSV文件中的一行,該CSV文件以逗號分隔各個字段。

Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA

需求:取得第三個字段”1234 A Pretty Street, NE”。

當字段中包含了字段分隔符時,直接使用FS劃分字段來處理會非常棘手。gawk為了解決這種特殊需求,提供了FPAT變量。

FPAT可以收集正則匹配的結果,並將它們保存在各個字段中。(就像grep匹配成功的部分會加顏色顯示,而使用FPAT劃分字段,則是將匹配成功的部分保存在字段$1 $2 $3...中)。

echo 'Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA' |\
awk 'BEGIN{FPAT="[^,]+|\".*\""}{print $1,$3}'

取字段中指定字符數量

16  001agdcdafasd
16  002agdcxxxxxx
23  001adfadfahoh
23  001fsdadggggg

得到:

16  001
16  002
23  001
23  002
awk '{print $1,substr($2,1,3)}'
awk 'BEGIN{FIELDWIDTH="2 2:3"}{print $1,$2}' a.txt

行列轉換

name age
alice 21
ryan 30

轉換得到:

name alice ryan
age 21 30
awk '
    {
      for(i=1;i<=NF;i++){
        if(!(i in arr)){
          arr[i]=$i
        } else {
            arr[i]=arr[i]" "$i
        }
      }
    }
    END{
        for(i=1;i<=NF;i++){
            print arr[i]
        }
    }
' a.txt

行列轉換2

文件內容:

74683 1001
74683 1002
74683 1011
74684 1000
74684 1001
74684 1002
74685 1001
74685 1011
74686 1000
....
100085 1000
100085 1001

文件就兩列,希望處理成

74683 1001 1002 1011
74684 1000 1001 1002
...

就是只要第一列數字相同, 就把他們的第二列放一行上,中間空格分開

{
  if($1 in arr){
    arr[$1] = arr[$1]" "$2
  } else {
    arr[$1] = $2
  }
  
}

END{
  for(i in arr){
    printf "%s %s\n",i,arr[i]
  }
}

篩選給定時間範圍內的日誌

grep/sed/awk用正則去篩選日誌時,如果要精確到小時、分鐘、秒,則非常難以實現。

但是awk提供了mktime()函數,它可以將時間轉換成epoch時間值。

# 2019-11-10 03:42:40轉換成epoch
$ awk 'BEGIN{print mktime("2019 11 10 03 42 40")}'
1573328560

藉此,可以取得日誌中的時間字符串部分,再將它們的年、月、日、時、分、秒都取出來,然後放入mktime()構建成對應的epoch值。因為epoch值是數值,所以可以比較大小,從而決定時間的大小。

下面strptime1()實現的是將2019-11-10T03:42:40+08:00格式的字符串轉換成epoch值,然後和which_time比較大小即可篩選出精確到秒的日誌。

下面strptime2()實現的是將10/Nov/2019:23:53:44+08:00格式的字符串轉換成epoch值,然後和which_time比較大小即可篩選出精確到秒的日誌。

BEGIN{
  # 要篩選什麼時間的日誌,將其時間構建成epoch值
  which_time = mktime("2019 11 10 03 42 40")
}

{
  # 取出日誌中的日期時間字符串部分
  match($0,"^.*\\[(.*)\\].*",arr)
  
  # 將日期時間字符串轉換為epoch值
  tmp_time = strptime2(arr[1])
  
  # 通過比較epoch值來比較時間大小
  if(tmp_time > which_time){
    print 
  }
}

# 構建的時間字符串格式為:"10/Nov/2019:23:53:44+08:00"
function strptime2(str   ,dt_str,arr,Y,M,D,H,m,S) {
  dt_str = gensub("[/:+]"," ","g",str)
  # dt_sr = "10 Nov 2019 23 53 44 08 00"
  split(dt_str,arr," ")
  Y=arr[3]
  M=mon_map(arr[2])
  D=arr[1]
  H=arr[4]
  m=arr[5]
  S=arr[6]
  return mktime(sprintf("%s %s %s %s %s %s",Y,M,D,H,m,S))
}

function mon_map(str   ,mons){
  mons["Jan"]=1
  mons["Feb"]=2
  mons["Mar"]=3
  mons["Apr"]=4
  mons["May"]=5
  mons["Jun"]=6
  mons["Jul"]=7
  mons["Aug"]=8
  mons["Sep"]=9
  mons["Oct"]=10
  mons["Nov"]=11
  mons["Dec"]=12
  return mons[str]
}

去掉/**/中間的註釋

示例數據:

/*AAAAAAAAAA*/
1111
222

/*aaaaaaaaa*/
32323
12341234
12134 /*bbbbbbbbbb*/ 132412

14534122
/*
    cccccccccc
*/
xxxxxx /*ddddddddddd
    cccccccccc
    eeeeeee
*/ yyyyyyyy
5642341

前後段落關係判斷

從如下類型的文件中,找出false段的前一段為i-order的段,同時輸出這兩段。

2019-09-12 07:16:27 [-][
  'data' => [
    'http://192.168.100.20:2800/api/payment/i-order',
  ],
]
2019-09-12 07:16:27 [-][
  'data' => [
    false,
  ],
]
2019-09-21 07:16:27 [-][
  'data' => [
    'http://192.168.100.20:2800/api/payment/i-order',
  ],
]
2019-09-21 07:16:27 [-][
  'data' => [
    'http://192.168.100.20:2800/api/payment/i-user',
  ],
]
2019-09-17 18:34:37 [-][
  'data' => [
    false,
  ],
]
BEGIN{
  RS="]\n"
  ORS=RS
}
{
  if(/false/ && prev ~ /i-order/){
    print tmp
    print
  }
  tmp=$0
}

兩個文件的處理

有兩個文件file1和file2,這兩個文件格式都是一樣的。

需求:先把文件2的第五列刪除,然後用文件2的第一列減去文件一的第一列,把所得結果對應的貼到原來第五列的位置,請問這個腳本該怎麼編寫?

file1:
50.481  64.634  40.573  1.00  0.00
51.877  65.004  40.226  1.00  0.00
52.258  64.681  39.113  1.00  0.00
52.418  65.846  40.925  1.00  0.00
49.515  65.641  40.554  1.00  0.00
49.802  66.666  40.358  1.00  0.00
48.176  65.344  40.766  1.00  0.00
47.428  66.127  40.732  1.00  0.00
51.087  62.165  40.940  1.00  0.00
52.289  62.334  40.897  1.00  0.00
file2:
48.420  62.001  41.252  1.00  0.00
45.555  61.598  41.361  1.00  0.00
45.815  61.402  40.325  1.00  0.00
44.873  60.641  42.111  1.00  0.00
44.617  59.688  41.648  1.00  0.00
44.500  60.911  43.433  1.00  0.00
43.691  59.887  44.228  1.00  0.00
43.980  58.629  43.859  1.00  0.00
42.372  60.069  44.032  1.00  0.00
43.914  59.977  45.551  1.00  0.00

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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