PL真有意思(四):控制流

前言

對大多數計算模型而言,順序都是基本的東西,它確定了為完成所期望的某種工作,什麼事情應該最先做,什麼事應該隨後做,我們可以將語言規定順序的機制分為幾個類別:

  • 順序執行
  • 選擇
  • 迭代
  • 過程抽象
  • 遞歸
  • 併發
  • 異常處理和推斷
  • 非確定性

對於不同類別的語言對不同類別的控制流的重要性也不盡相同,比如順序執行相比於函數式對於命令式則更加重要。而命令式中更傾向用迭代,函數則更強調遞歸

表達式求值

在討論控制流之前先討論下錶達式的問題,先明確兩個概念:運算符通常是指那些採用特殊語法形式的內部函數(比如+-*/等),運算對象指的是運算符的參數(如2+3,2和3就是運算對象),那麼運算符和運算對象的組合就是表達式。一般根據運算符出現的位置(相對於運算對象而言),可以分為3類表示形式:前綴、中綴和後綴。比如Lisp就運用前綴語法:

(+ 1 3 4 6)      
(* (+ 1 7) 8)    

大多數命令式語言對二元運算符都使用中綴記法,而對一元運算符和其它函數使用前綴激發。但是像Lisp就全部統一使用中綴記法

優先級和結合性

大多數程序設計語言都提供豐富的內部算術。在用中綴方式(沒有括號)寫出就可能出現歧義。所以就需要優先級和結合性來解決歧義性,但是我覺得

媽的你寫括號就完事兒了

而且不同語言的優先級和結合性也不盡相同

賦值

在純函數式語言中,程序的基本組成部分是表達式,計算也僅是對表達式求值。任何一個表達式對於整個計算的影響也僅限於這個表達式所處的上下文環境。

而命令式語言的情況與此截然不同,計算通常是通過對內存中變量值的一系列修改操作來完成,賦值就是這種修改的最基本手段。每一次賦值都表示一個值被放入一個對應的變量中。

一般來說,如果語言中的一個結構除了返回一個值供其外圍環境所使用,還能以其他方式影響後續計算(並最終影響程序輸出),那麼我們就說這種結構有副作用。而副作用也是命令式語言里最核心的部分

而在純函數語言中沒有任何的副作用,表達式的值只依賴於輸入

但是現在許多語言都是混合的,像Python和Ruby主要是命令式的,但是也提供了很多的函數式的特徵,現在連Java都提供了對函數式的支持

引用和值

考慮一下下面的C語言的賦值:

d = a;
a = b + c;

第一個語句中,賦值語句右部引用了a的值,並希望把這個值放入d。第二個語句左部引用了a的位置,希望把b+c的結果放進去。這兩種解釋(值和位置)都是可行的,因為c語言中變量就是能保存值的命名容器,所以我們會說類似的語言的變量是值模型。由於指示位置的表達式被放在賦值語句的左部,所以這種指示位置的表達式成為左值表達式。表示一個值的表達式稱為右值。在變量的值模型下,同一表達式也可能是左值或者右值,比如(a=a+1),左部的a是左值,用於表示存放結果的位置;右部的a是右值,用於代表a具體所指的值。

在採用了變量的引用模型的語言中,這種左值和右值的差異就更加明顯了。

b = 2;
c = b;
a = b + c;

在值模型語言中程序員會說:“把2放入b,然後複製到c,然後用它們兩個的值相加,把結果4放入a。”。;

在引用模型語言中的程序員會說:“讓b引用2,讓c也引用2,然後把這兩個引用送給+運算,並讓a引用算出的結果,也是4“。

而在Java中,對於內部類型使用值模型,而類使用引用模型

裝箱

對於內部類型使用值模型,就無法以統一的方式將它們傳給要求類類型的參數的方法,所以這裏就需要一個裝箱過程

比如Java提供的Integer類

Integer i = new Integer(12);

多路賦值

我們知道賦值操作有右結合性,這使得我們可以寫出a=b=c的簡練代碼,在一些語言中(Ruby,Go,Python)我們可以進一步這樣寫:

a, b = 1, 2;
//上面的語句結果就是a等於1,b等於2。

a, b = b, a;
//交換兩個值,如果沒有這種語言特性,那麼就需要引入臨時變量了。

a, b , c = funx(d, e, f);

這種記法也消除了大多數程序設計語言中函數的非對稱性,這些語言可以允許任意多個參數,但只能返回一個返回值。但是其實在Python中的返回多個值,就是將多個值封裝為元組,在賦值的時候又拆箱而已

初始化

並不是所有語言都提供聲明變量時指定初始值的方式,但是至少有這幾點可以證明提供初始值的機制是有益的

  • 局部靜態變量需要一個初始值才能使用
  • 使用靜態分配的變量,可以由編譯器放入全局內存,避免了在運行時賦予吃數值所造成的開銷
  • 可以避免意外的使用未初始的變量

如果聲明時沒有明確的給定變量的初始值,語言也可以給定一個默認值。像C、Java和C#也都提供了類似的機制

動態檢查

除了可以指定默認值之外,還可以採用另外一種方式,將對為初始化的變量的使用作為動態語義錯誤,在運行時捕獲這種錯誤。但是在運行時捕獲所有使用到未初始化的情況的代價非常高

定義性賦值

在Java和C#中提供了一種定義性賦值的表示形式,意思就是由編譯器來檢查在達到一個表達式的所有可能控制路徑上,都必須為這個表達式中的每個變量賦過值

構造函數

許多面向對象語言都提供了為用戶定義的類型的自動初始化方法,也就是構造函數

在C++中,還區分了初始化和賦值,它將初始化賦值解釋為調用變量所屬類型的構造函數,以初始值作為調用參數。在沒有強制的情況下,賦值被解釋為調用相關類型的賦值運算符,如果沒有定義賦值運算符,就默認將賦值右部的值簡單的按位複製過來

區分初始化和賦值的好處是,可以區分在賦值前是不是需要先釋放空間

表達式的順序問題

雖然優先級和結合性規則定義了表達式里二元中綴運算符的應用順序,但卻沒有明確說明特定運算符的各運算對象的求值順序。舉例來說,如下錶達式:

 a - f(b) - c * d

根據結合性可知a-f(b)將在第二個減法前執行,根據優先級可知第二個減法的右運算對象是cd這個整體而不是c。但是如果沒有進一步的規則描述,我們無法得知a-f(b)是否在cd之前運行。諸如此類:對於f(a,g(b),c)這個子程序調用,我們也不知這三個參數的求值順序。

求值順序之所以重要:

  • 副作用:如果f(b)這個子程序可能會修改c的值,那麼a-f(b)-cd的求值結果將依賴f(b)和cd哪一個先執行;類似的,如果g(b)修改了a或者c的值,那麼f(a,g(b),c)的結果也是依賴於參數的求值順序。

  • 代碼改進:子表達式的求值順序對於寄存器分配和指令調度都有重要的影響。比如(ab+f(c)),我們可能會希望在執行ab之前調用f(c)。因為如果先計算乘法,則在調用f(c)之前就要先保存起來乘積,因為f(c)可能會用光所有的寄存器。

短路求值

對於布爾表達式,如果編譯器可以對其執行短路求值,那麼它生成的代碼可以在表達式前一半的結果可以確定整個表達式的值的情況下跳過後一半的計算。

比如(a<b) and(b<c),如果a>b,那麼完全沒必要去檢查b是否小於c就可以確定這個表達式一定為假。在一些特殊情況下,短路求值可節省大量時間,比如if(func&&func())。實際上這種情況下短路求值已經改變了布爾表達式的語義,如果非短路求值,那麼在func不存在的情況下去執行func(),程序是會拋出錯誤的。

我們常見的語法表現形式是&&和||這種布爾運算符身兼多職,既是布爾運算符又會觸發短路求值,但是有一些語言針對短路求值是有單獨的語法形式的,比如Clu語言中布爾運算符是and和or,短路運算符是cand和cor。這是為何呢,因為有些代碼邏輯是不需要這種短路求值的優化的。

結構化和非結構化的流程

彙編語言中的控制流通過有條件的或無條件的跳轉(分支)指令來完成,早期的高級語言模仿這種方式(如Fortran),主要依賴goto來描述大部分非過程化控制流,比如下面代碼:

if A < B goto label1;

label1;

但是如今goto像在Java、Clu和Eiffel里已經完全被禁止了,在其它語言也是受限了或者只是為了向前兼容而已

goto的結構化替代品

對於goto被廢棄,各種使用goto的地方也被結構的方案給代替了

  • 循環中的退出和繼續

break和contiune這兩個關鍵字大家應該很熟悉了

  • 從子程序提前返回

return

  • 多層返回

上面的兩個問題都可以有很好的替代品,但是對於多層返回就會比較麻煩一點。return或”局部的goto“只能在子程序中返回,如果遇到多層嵌套的子程序,想從內層的子程序返回來結束外圍子程序的執行,那return和局部的goto就無能為力了。這種情況下,語言實現要保證能恰當的恢復棧上的子程序調用信息,這種修復工作稱為”回卷”,為完成此事,不僅必須釋放需要跳出的所有子程序的棧幀,還要執行一些信息管理工作,比如恢復寄存器內容。

Common Lisp提供了return-from語句來明確指定需要退出的詞法外圍函數或嵌套塊,還可以提供一個返回值:

Common Lisp和另外一個語言Ruby中還內置一個throw/catch語法來支持這種多層返回,注意這種結構並不是所謂的異常處理,而是一種多層返回的語法結構,直白點說是一種功能強大的變相”goto“,看下面代碼:

//定義一個方法
def search_file(filename,pattern)
   file=File.Open(filename)
   //遍歷文件每一行
   file.each{|line|
        //根據parrern匹配模式查找,如果匹配就返回到定義found標籤的位置
        throw :found,line if line=~/#{pattern}/
   }
end

//用catch定義一個found標籤
math=catch:found do
   serach_file("f1",key) 
   serach_file("f2",key)    //如果f2文件找到了則就會返回line至math
   serach_file("f3",key)
   ”not fount“              //找不到就執行到此處了
end

print match
  • 錯誤和異常

多層返回的概念假定了被調用方知道調用方期的是什麼,並且能返回一個適當的值。還存在一種情況,其中深層嵌套的子程序中發生了一些情況,導致無法繼續執行下去,而且因為沒有足夠的環境信息,甚至無法合適的結束自己的工作,這種情況下,唯一能做的就是”退回去“,一直回退到能夠恢復執行的地方,這種要求程序退回去的條件通常稱為叫做”異常“。常見的結構化的異常處理和多層返回有很大的相似性,兩者都需要從某一個內層上下文回退到外層的上下文。具體的差異則是多層返回是內層的上下文正常的完成計算然後根據需要返回正確的值,然後轉移到外層上下文,並不需要後續處理。而異常中的內層上下文已經是無法進行正常的計算,必須以一種非正常的退出一直回卷,然後觸發某個特殊的處理流程直到catch到它。

  • 繼續

如果進一步推廣上一小節中造成棧回卷的非局部goto概念,則可以定義一種稱為繼續(Continuations)的概念。從底層來看,一個繼續是由一個代碼地址與其關聯的一個引用環境組成的,如果跳轉到這個地址,就該恢復這個引用環境。從抽象層面看,它描述一個可能由此繼續下去的執行上下文。在Scheme和Ruby中,繼續是基本的一等公民,我們可以利用這種機制有效的擴充流程控制結構集合。

Scheme中支持繼續由一個通常稱為call-with-current-continuation的函數實現,有時簡稱”call/cc”。該函數有一個參數f,f也是一個函數;”call/cc”調用函數f,把一個記錄著當前程序計數器和引用環境的“繼續(暫時稱為是c)c”傳遞給f,這種”繼續c”由一個閉包來表示(通過參數傳遞的子程序的表示的閉包並無不同)。在將來任何時候,f都可以調用c,然後可以用c來重新建立其保存的上下文。一般的應用情況是我們把這個c賦值給一個變量,則可重複的調用它,甚至我們可以在f中返回它,即使f已經執行完畢,仍然可以調用c。

順序執行

選擇

現在大部分命令式語言中採用的選擇語句,都是從Algol 60引進過的 if…then…else 的某種演進變形:

if condition then statement
else if condition then statement
else if condition then statement
...
else statement

短路條件

雖然 if…then…else 語句的條件是一個布爾表達式,但是通常沒有必要求出這個表達式的值放入寄存器。大部分機器都提供了條件分支指令(如上面提到的IL指令brtrue.s),因為這個表達式求值的目的並不是為了值,而是為了跳轉到合適的位置。這種看法使得可以對短路求值的表達式生成高效的代碼(稱為跳轉碼)。跳轉碼不但可以用於選擇語句,也可用在“邏輯控制的循環”中。如下面代碼:

if((A>B)&&(C<D)||(E!=F)){
    then_clause
}
else{
    else_clause
}

在不使用短路求值的Pascal中,生成的代碼大致如下(它會計算每個表達式的結果並放入寄存器r1…,然後再決定跳轉):

     r1=A
     r2=B
     r1=r1>r2
     r2=C
     r3=D
     r2=r2>r3
     r1=r1&r2
     r2=E
     r3=F
     r2=r2!=r3
     r1=r1|r2
     if r1=0 goto L2
L1: then_clause
    goto L3
L2: else_clause
L3:

跳轉碼的情況於此不同,它不會把表達式的值存入寄存器,而是直接跳轉(只用到了r1和r2兩個寄存器,明顯也不會針對整個表達式進行求值,比上面的要高效一些):

     r1=A
     r2=B
     if r1<=r2 goto L4
     r1=C
     r2=D
     if r1>r2 goto L1
L4: r1=E
     r2=F
     if r1=r2 goto L2
L1: then_clause
    goto L3
L2: else_clause
L3:

case/switch語句

對於if else結構來說,如果嵌套的層數過多、或者是用於判斷的條件表達式是基於一些有限的簡單值(或編譯時常量),那麼出現了一種更為優雅的語法結構“case語句”,有很多ifelse都可以很輕鬆的改寫成case/switch語句

對於case/switch的優勢還不只是語法上的優雅,有時還可以生成更高效的代碼

T: &L1
   &L2
   &L3
   &L4
   &L5
   &L6
L1: clause_A
    goto L7
L2: clause_B
    goto L7
L3: clause_C
    goto L7
L4: clause_D
    goto L7
L5: clause_E
    goto L7
L6: clause_F
    goto L7
L7:

這樣其實T就是一個地址跳轉表

迭代

迭代和遞歸是計算機能夠重複執行一些操作的兩種機制;命令式語言傾向於使用迭代、函數式語言則更看重遞歸。大多數語言中的迭代都是以循環的形式出現的,和複合語句中的語句一樣,迭代的執行通常也是為了副作用,也就是修改一些變量的值。根據用何種方式控制迭代的次數來看,循環有兩個主要變種”枚舉控制的循環”和“邏輯控制的循環”。前者是在給定的某個有限的集合中執行,後者則是不確定要執行多少次(直到它所依賴的表達式結果被改變)。對於這兩種結構,大多數的語言都提供了不同的語法結構來表示。

枚舉控制的循環

枚舉控制的循環最初來自Fortran的do循環,

do i = 1, 10, 2
  ...
enddo

等號後面的表達式分別是i的初始值,邊界值和步長

像這種枚舉循環可以說的不多,但是如果前幾次迭代的執行會導致迭代的次數或下標值的發生變化,那麼我們就需要一個更通用的實現

思考幾個問題:

  • 控制是否可以通過枚舉之外的任何方式進入和離開循環呢?
  • 如果循環體修改了用於計算循環結束邊界的變量,會發生什麼?
  • 如果循環體修改了下標變量,會發生?
  • 程序是否可以在循環結束後讀取下標變量,如果可以,它的值將是什麼?
  1. 現在的大多數語言都提供了,break類似的機制來離開循環。Fortran IV允許通過goto跳入到一個循環中,但是這個通常被認為是一個語言缺陷
  2. 同樣的,在大多數語言中,邊界值只在第一次計算,並且保存在一個臨時寄存器中,所以對於之後的修改並不會起作用
  3. 早期的大部分語言都禁止在枚舉控制的循環中修改下邊變量。但是剛剛試驗了一下,許多的語言好像都放開了這個禁止,也就是按照修改后的正常邏輯繼續執行
  4. 首先這是一個語言實現的問題,現在的大多數語言應該都是將循環下標的作用域限定在循環體內了,所以出了循環體是訪問不到的

當然在之後出現了C的for循環

for (int i = first; i < last; i += step) {
  ...
}

這樣有關結束條件、溢出和循環方向的問題全都交由程序員來掌控

迭代器

上面描述的循環都是在算術值的序列上迭代。不過一般而言,我們還希望可以在任何定義的良好的集合的元素上迭代。在C++和Java里叫做迭代器

真迭代器

Clu,Ruby等語言允許任何容器對象提供一個枚舉自己元素的迭代器,這種迭代器就像是允許包含yield語句的子程序,每次yield生成一個循環下標

在Python里就可以這樣寫

for i in range(first, last, step):
    ...

在被調用時,這個迭代器算出循環的第一個下標值,然後通過yield語句返回給調用者,yield語句很像return,但是不同的是再每次循環結束后控制權會再次的交給迭代器,重新開始下一次yield,直到迭代器沒有元素可yield為止才結束for循環。從效果上看迭代器就像是另外一個控制線程,有它自己的程序計數器,它的執行與為它提供下標值的for循環交替執行,這一類通常稱為真迭代器。

迭代器

在許多面向對象語言里,用了更加面向對象的方法來實現迭代器。它們的迭代器就是一個常規對象,它提供了一套方法,用來初始化、生成下一個下標值和檢測結束條件

BinTree<Integer> myTree;

for (Integer i : myTreee) {

}

上面的這段代碼其實是下面這段的一個語法糖

for(Iterator<Integer> it = myTree.iterator();it.hasNext();) {

}

用一級函數來實現迭代器

實現是將循環的體寫成一個函數,用循環的下標作為函數的參數,然後將這函數作為參數傳遞給一個迭代器

(define uptoby
    (lambda (low high step f)
        (if (<= low higt)
            (begin
                (f low)
                (uptoby (+ low step) high step f))
            '())))

不用迭代器的迭代

在那些沒有真迭代器或者迭代器對象的語言中,還是可以通過編程方式實現集合枚舉和使用元素之間的解耦的,用C語言做例子:

tree_node *my_tree;    
tree_iter ti:                 
...
for(ti_create(my_tree,&ti);
              !ti_done(ti);
              ti_next(&ti)){
     tree_node *n=ti_val(ti);
     ...
}
ti_delete(&ti);

邏輯控制的循環

和枚舉循環相比,邏輯控制的循環關注點只在結束條件上

前置檢測

由Algol W引進,後來被Pascal保留

while cond do stat

後置檢測

這種的循環體不管是否滿足循環條件,都至少會執行一次循環體。如C語言的do while語句

do{
   line=read_line();
   //...代碼
} while line[0]!='$'; 

中置檢測

中置檢測一般依賴if

for(;;){
   line=read_line();
   if line[0]!='$' break;
}

遞歸

遞歸和上述討論的其他控制流都不同,它不依賴特殊的語法形式,只要語言允許函數直接或間接的調用自身,那麼就是支持遞歸的。大部分情況下遞歸和迭代都可以互相用對方重寫的。

迭代和遞歸

早期的一些語言不支持遞歸(比如Fortan77以前的版本),也有一些函數式語言不允許迭代,然而大部分現代語言都是同時支持兩者的。在命令式語言中,迭代在某種意義上顯得更自然一些,因為它們的核心就是反覆修改一些變量;對於函數式語言,遞歸更自然一些,因為它們並不修改變量。如果是要計算gcd(更相減損法),遞歸更自然一些:

int gcd(int a,int b){
  if(a==b) return a;
  else if (a>b) return gcd(a-b,b);
  else return gcd(a,b-a);
}

用迭代則是這樣:

int gcd(int a,int b){
   while(a!=b){
      if(a>b) a=a-b;
      else  b=b-a;
   }
   return a;
}

尾遞歸

經常有人說迭代比遞歸效率更高,其實更準確的說法應該是,迭代的樸素實現的(無優化)效率通常比遞歸的樸素實現的效率要高。如上面gcd的例子,如果遞歸的實現確實是實實在在的子程序調用,那麼這種子程序調用所帶來的棧的分配等的開銷確實要比迭代要大。然而一個“優化”的編譯器(通常是專門為函數式語言設計的編譯器),常常能對遞歸函數生成優異的代碼,如上面的gcd尾遞歸(尾遞歸函數是指在遞歸調用之後再無其他計算的函數,其返回值就是遞歸調用的返回值)。對這種函數完全不必要進行動態的棧分配,編譯器在做遞歸調用時可以重複使用當前的棧空間,從效果上看,好的編譯器可以把上面遞歸的gcd函數改造為:

int gcd(int a,int b){
start:
   if (a==b) return a;
   else if (a>b){
     a=a-b;
     goto start;  
   }
   else{
     b=b-a;
     goto start;
  }
}

即使是那些非尾遞歸函數,通過簡單的轉換也可能產生出尾遞歸代碼。

應用序和正則序求值

在上述的討論中,我們都假定所有參數在傳入子程序之前已經完成了求值,但是實際中這並不是必須的。完全可以採用另外一種方式,把為求值的之際參數傳遞給子程序,僅在需要某個值得時候再去求它。前一種在調用前求值的方案稱為應用序求值;后一種到用時方求值的方式稱為正則序求值。正則序求值在宏這個概念中是自然而然的方式,前面討論的短路求值、以及後面要討論的按名調用參數也是應用的正則序求值,一些函數式語言中偶爾也會出現這種方式。

但是我們來看一個例子:

#define MAX(a,b) ((a)>(b)?(a):(b))

如果我這麼調用MAX(i++,j++),導致i和j都執行兩次++,產生了兩次副作用,這是我們不願意看到的結果。總結來說,只有在表達式求值不會產生副作用的情況下正則序才是安全的。

惰性求值

從清晰性和高效的角度看,應用序求值通常會比正則序合適一些,一次大部分語言都採用如此的方式。然而也確實有一些特殊情況下正則序更高效一些,而應用序會造成一些錯誤出現,這種情況的出現時因為一些參數的值實際上並不會被需要,但是還是被求值了,應用序求值有時也成為非惰性求值,比如下面的JavaScript代碼就會是一個死循環:

function while1() {
    while (true) { console.log('死循環')}
}
function NullFunction() { }
console.log(NullFunction(1,2,3,while1()));

Scheme通過內部函數delay和force提供可選的正則序求值功能,這兩個函數提供的實際上是惰性求值的一種實現

惰性求值最常見的一種用途就是用來創建無窮數據結構

(define naturals
    (letrec ((next (lambda (n) (cons n (delay (next (+ n 1)))))))
    (next 1)))

這樣就可以用Scheme表述所有的自然數

小結

本篇首先從表達式開始,介紹了表達式(語句)中的一些基本概念;然後從討論了從彙編時代到結構化程序設計時代語言中的控制流程的演進以及發展;有了前面兩個基礎,後面就詳細的介紹了程序中的三大基本流程控制結構順序、選擇、循環(遞歸和迭代)。

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

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

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Spring Boot 2.X(十八):集成 Spring Security-登錄認證和權限控制

前言

在企業項目開發中,對系統的安全和權限控制往往是必需的,常見的安全框架有 Spring Security、Apache Shiro 等。本文主要簡單介紹一下 Spring Security,再通過 Spring Boot 集成開一個簡單的示例。

Spring Security

什麼是 Spring Security?

Spring Security 是一種基於 Spring AOP 和 Servlet 過濾器 Filter 的安全框架,它提供了全面的安全解決方案,提供在 Web 請求和方法調用級別的用戶鑒權和權限控制。

Web 應用的安全性通常包括兩方面:用戶認證(Authentication)和用戶授權(Authorization)。

用戶認證指的是驗證某個用戶是否為系統合法用戶,也就是說用戶能否訪問該系統。用戶認證一般要求用戶提供用戶名和密碼,系統通過校驗用戶名和密碼來完成認證。

用戶授權指的是驗證某個用戶是否有權限執行某個操作。

2.原理

Spring Security 功能的實現主要是靠一系列的過濾器鏈相互配合來完成的。以下是項目啟動時打印的默認安全過濾器鏈(集成5.2.0):

[
    org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@5054e546,
    org.springframework.security.web.context.SecurityContextPersistenceFilter@7b0c69a6,
    org.springframework.security.web.header.HeaderWriterFilter@4fefa770,
    org.springframework.security.web.csrf.CsrfFilter@6346aba8,
    org.springframework.security.web.authentication.logout.LogoutFilter@677ac054,
    org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@51430781,
    org.springframework.security.web.savedrequest.RequestCacheAwareFilter@4203d678,
    org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@625e20e6,
    org.springframework.security.web.authentication.AnonymousAuthenticationFilter@19628fc2,
    org.springframework.security.web.session.SessionManagementFilter@471f8a70,
    org.springframework.security.web.access.ExceptionTranslationFilter@3e1eb569,
    org.springframework.security.web.access.intercept.FilterSecurityInterceptor@3089ab62
]
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CsrfFilter
  • LogoutFilter
  • UsernamePasswordAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • AnonymousAuthenticationFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor

詳細解讀可以參考:https://blog.csdn.net/dushiwodecuo/article/details/78913113

3.核心組件

SecurityContextHolder

用於存儲應用程序安全上下文(Spring Context)的詳細信息,如當前操作的用戶對象信息、認證狀態、角色權限信息等。默認情況下,SecurityContextHolder 會使用 ThreadLocal 來存儲這些信息,意味着安全上下文始終可用於同一執行線程中的方法。

獲取有關當前用戶的信息

因為身份信息與線程是綁定的,所以可以在程序的任何地方使用靜態方法獲取用戶信息。例如獲取當前經過身份驗證的用戶的名稱,代碼如下:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
    String username = ((UserDetails)principal).getUsername();
} else {
    String username = principal.toString();
}

其中,getAuthentication() 返回認證信息,getPrincipal() 返回身份信息,UserDetails 是對用戶信息的封裝類。

Authentication

認證信息接口,集成了 Principal 類。該接口中方法如下:

接口方法 功能說明
getAuthorities() 獲取權限信息列表,默認是 GrantedAuthority 接口的一些實現類,通常是代表權限信息的一系列字符串
getCredentials() 獲取用戶提交的密碼憑證,用戶輸入的密碼字符竄,在認證過後通常會被移除,用於保障安全
getDetails() 獲取用戶詳細信息,用於記錄 ip、sessionid、證書序列號等值
getPrincipal() 獲取用戶身份信息,大部分情況下返回的是 UserDetails 接口的實現類,是框架中最常用的接口之一

AuthenticationManager

認證管理器,負責驗證。認證成功后,AuthenticationManager 返回一個填充了用戶認證信息(包括權限信息、身份信息、詳細信息等,但密碼通常會被移除)的 Authentication 實例。然後再將 Authentication 設置到 SecurityContextHolder 容器中。

AuthenticationManager 接口是認證相關的核心接口,也是發起認證的入口。但它一般不直接認證,其常用實現類 ProviderManager 內部會維護一個 List<AuthenticationProvider> 列表,存放里多種認證方式,默認情況下,只需要通過一個 AuthenticationProvider 的認證,就可被認為是登錄成功。

UserDetailsService

負責從特定的地方加載用戶信息,通常是通過JdbcDaoImpl從數據庫加載實現,也可以通過內存映射InMemoryDaoImpl實現。

UserDetails

該接口代表了最詳細的用戶信息。該接口中方法如下:

接口方法 功能說明
getAuthorities() 獲取授予用戶的權限
getPassword() 獲取用戶正確的密碼,這個密碼在驗證時會和 Authentication 中的 getCredentials() 做比對
getUsername() 獲取用於驗證的用戶名
isAccountNonExpired() 指示用戶的帳戶是否已過期,無法驗證過期的用戶
isAccountNonLocked() 指示用戶的賬號是否被鎖定,無法驗證被鎖定的用戶
isCredentialsNonExpired() 指示用戶的憑據(密碼)是否已過期,無法驗證憑證過期的用戶
isEnabled() 指示用戶是否被啟用,無法驗證被禁用的用戶

Spring Security 實戰

1.系統設計

本文主要使用 Spring Security 來實現系統頁面的權限控制和安全認證,本示例不做詳細的數據增刪改查,sql 可以在完整代碼里下載,主要是基於數據庫對頁面 和 ajax 請求做權限控制。

1.1 技術棧

  • 編程語言:Java
  • 編程框架:Spring、Spring MVC、Spring Boot
  • ORM 框架:MyBatis
  • 視圖模板引擎:Thymeleaf
  • 安全框架:Spring Security(5.2.0)
  • 數據庫:MySQL
  • 前端:Layui、JQuery

1.2 功能設計

  1. 實現登錄、退出
  2. 實現菜單 url 跳轉的權限控制
  3. 實現按鈕 ajax 請求的權限控制
  4. 防止跨站請求偽造(CSRF)攻擊

1.3 數據庫層設計

t_user 用戶表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
username varchar 20 用戶名
password varchar 255 密碼

t_role 角色表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
role_name varchar 20 角色名稱

t_menu 菜單表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
menu_name varchar 20 菜單名稱
menu_url varchar 50 菜單url(Controller 請求路徑)

t_user_roles 用戶權限表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
user_id int 8 用戶表id
role_id int 8 角色表id

t_role_menus 權限菜單表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
role_id int 8 角色表id
menu_id int 8 菜單表id

實體類這裏不詳細列了。

2.代碼實現

2.0 相關依賴

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!-- 熱部署模塊 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional> <!-- 這個需要為 true 熱部署才有效 -->
        </dependency>
        
            <!-- mysql 數據庫驅動. -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- mybaits -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
        
        <!-- thymeleaf -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        
        <!-- alibaba fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <!-- spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>

2.1 繼承 WebSecurityConfigurerAdapter 自定義 Spring Security 配置

/**
prePostEnabled :決定Spring Security的前註解是否可用 [@PreAuthorize,@PostAuthorize,..]
secureEnabled : 決定是否Spring Security的保障註解 [@Secured] 是否可用
jsr250Enabled :決定 JSR-250 annotations 註解[@RolesAllowed..] 是否可用.
 */
@Configurable
@EnableWebSecurity
//開啟 Spring Security 方法級安全註解 @EnableGlobalMethodSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;
    @Autowired
    private UserDetailsService userDetailsService;
    
    /**
     * 靜態資源設置
     */
    @Override
    public void configure(WebSecurity webSecurity) {
        //不攔截靜態資源,所有用戶均可訪問的資源
        webSecurity.ignoring().antMatchers(
                "/",
                "/css/**",
                "/js/**",
                "/images/**",
                "/layui/**"
                );
    }
    /**
     * http請求設置
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //http.csrf().disable(); //註釋就是使用 csrf 功能       
        http.headers().frameOptions().disable();//解決 in a frame because it set 'X-Frame-Options' to 'DENY' 問題           
        //http.anonymous().disable();
        http.authorizeRequests()
            .antMatchers("/login/**","/initUserData","/main")//不攔截登錄相關方法        
            .permitAll()        
            //.antMatchers("/user").hasRole("ADMIN")  // user接口只有ADMIN角色的可以訪問
//          .anyRequest()
//          .authenticated()// 任何尚未匹配的URL只需要驗證用戶即可訪問
            .anyRequest()
            .access("@rbacPermission.hasPermission(request, authentication)")//根據賬號權限訪問         
            .and()
            .formLogin()
            .loginPage("/")
            .loginPage("/login")   //登錄請求頁
            .loginProcessingUrl("/login")  //登錄POST請求路徑
            .usernameParameter("username") //登錄用戶名參數
            .passwordParameter("password") //登錄密碼參數
            .defaultSuccessUrl("/main")   //默認登錄成功頁面
            .and()
            .exceptionHandling()
            .accessDeniedHandler(customAccessDeniedHandler) //無權限處理器
            .and()
            .logout()
            .logoutSuccessUrl("/login?logout");  //退出登錄成功URL
            
    }
    /**
     * 自定義獲取用戶信息接口
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
    
    /**
     * 密碼加密算法
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
 
    }
}

2.2 自定義實現 UserDetails 接口,擴展屬性

public class UserEntity implements UserDetails {

    /**
     * 
     */
    private static final long serialVersionUID = -9005214545793249372L;

    private Long id;// 用戶id
    private String username;// 用戶名
    private String password;// 密碼
    private List<Role> userRoles;// 用戶權限集合
    private List<Menu> roleMenus;// 角色菜單集合

    private Collection<? extends GrantedAuthority> authorities;
    public UserEntity() {
        
    }
    
    public UserEntity(String username, String password, Collection<? extends GrantedAuthority> authorities,
            List<Menu> roleMenus) {
        this.username = username;
        this.password = password;
        this.authorities = authorities;
        this.roleMenus = roleMenus;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public List<Role> getUserRoles() {
        return userRoles;
    }

    public void setUserRoles(List<Role> userRoles) {
        this.userRoles = userRoles;
    }

    public List<Menu> getRoleMenus() {
        return roleMenus;
    }

    public void setRoleMenus(List<Menu> roleMenus) {
        this.roleMenus = roleMenus;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}

2.3 自定義實現 UserDetailsService 接口

/**
 * 獲取用戶相關信息
 * @author charlie
 *
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    private Logger log = LoggerFactory.getLogger(UserDetailServiceImpl.class);

    @Autowired
    private UserDao userDao;

    @Autowired
    private RoleDao roleDao;
    @Autowired
    private MenuDao menuDao;

    @Override
    public UserEntity loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根據用戶名查找用戶
        UserEntity user = userDao.getUserByUsername(username);
        System.out.println(user);
        if (user != null) {
            System.out.println("UserDetailsService");
            //根據用戶id獲取用戶角色
            List<Role> roles = roleDao.getUserRoleByUserId(user.getId());
            // 填充權限
            Collection<SimpleGrantedAuthority> authorities = new HashSet<SimpleGrantedAuthority>();
            for (Role role : roles) {
                authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
            }
            //填充權限菜單
            List<Menu> menus=menuDao.getRoleMenuByRoles(roles);
            return new UserEntity(username,user.getPassword(),authorities,menus);
        } else {
            System.out.println(username +" not found");
            throw new UsernameNotFoundException(username +" not found");
        }       
    }

}

2.4 自定義實現 URL 權限控制

/**
 * RBAC數據模型控制權限
 * @author charlie
 *
 */
@Component("rbacPermission")
public class RbacPermission{

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        Object principal = authentication.getPrincipal();
        boolean hasPermission = false;
        // 讀取用戶所擁有的權限菜單
        List<Menu> menus = ((UserEntity) principal).getRoleMenus();
        System.out.println(menus.size());
        for (Menu menu : menus) {
            if (antPathMatcher.match(menu.getMenuUrl(), request.getRequestURI())) {
                hasPermission = true;
                break;
            }
        }
        return hasPermission;
    }
}

2.5 實現 AccessDeniedHandler

自定義處理無權請求

/**
 * 處理無權請求
 * @author charlie
 *
 */
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private Logger log = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException, ServletException {
        boolean isAjax = ControllerTools.isAjaxRequest(request);
        System.out.println("CustomAccessDeniedHandler handle");
        if (!response.isCommitted()) {
            if (isAjax) {
                String msg = accessDeniedException.getMessage();
                log.info("accessDeniedException.message:" + msg);
                String accessDenyMsg = "{\"code\":\"403\",\"msg\":\"沒有權限\"}";
                ControllerTools.print(response, accessDenyMsg);
            } else {
                request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException);
                response.setStatus(HttpStatus.FORBIDDEN.value());
                RequestDispatcher dispatcher = request.getRequestDispatcher("/403");
                dispatcher.forward(request, response);
            }
        }

    }

    public static class ControllerTools {
        public static boolean isAjaxRequest(HttpServletRequest request) {
            return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        }

        public static void print(HttpServletResponse response, String msg) throws IOException {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write(msg);
            writer.flush();
            writer.close();
        }
    }

}

2.6 相關 Controller

登錄/退出跳轉

/**
 * 登錄/退出跳轉
 * @author charlie
 *
 */
@Controller
public class LoginController {
    @GetMapping("/login")
    public ModelAndView login(@RequestParam(value = "error", required = false) String error,
            @RequestParam(value = "logout", required = false) String logout) {
        ModelAndView mav = new ModelAndView();
        if (error != null) {
            mav.addObject("error", "用戶名或者密碼不正確");
        }
        if (logout != null) {
            mav.addObject("msg", "退出成功");
        }
        mav.setViewName("login");
        return mav;
    }
}

登錄成功跳轉

@Controller
public class MainController {

    @GetMapping("/main")
    public ModelAndView toMainPage() {
        //獲取登錄的用戶名
        Object principal= SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String username=null;
        if(principal instanceof UserDetails) {
            username=((UserDetails)principal).getUsername();
        }else {
            username=principal.toString();
        }
        ModelAndView mav = new ModelAndView();
        mav.setViewName("main");
        mav.addObject("username", username);
        return mav;
    }
    
}

用於不同權限頁面訪問測試

/**
 * 用於不同權限頁面訪問測試
 * @author charlie
 *
 */
@Controller
public class ResourceController {

    @GetMapping("/publicResource")
    public String toPublicResource() {
        return "resource/public";
    }
    
    @GetMapping("/vipResource")
    public String toVipResource() {
        return "resource/vip";
    }
}

用於不同權限ajax請求測試

/**
 * 用於不同權限ajax請求測試
 * @author charlie
 *
 */
@RestController
@RequestMapping("/test")
public class HttptestController {

    @PostMapping("/public")
    public JSONObject doPublicHandler(Long id) {
        JSONObject json = new JSONObject();
        json.put("code", 200);
        json.put("msg", "請求成功" + id);
        return json;
    }

    @PostMapping("/vip")
    public JSONObject doVipHandler(Long id) {
        JSONObject json = new JSONObject();
        json.put("code", 200);
        json.put("msg", "請求成功" + id);
        return json;
    }
}

2.7 相關 html 頁面

登錄頁面

<form class="layui-form" action="/login" method="post">
            <div class="layui-input-inline">
                <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
                <input type="text" name="username" required
                    placeholder="用戶名" autocomplete="off" class="layui-input">
            </div>
            <div class="layui-input-inline">
                <input type="password" name="password" required  placeholder="密碼" autocomplete="off"
                    class="layui-input">
            </div>
            <div class="layui-input-inline login-btn">
                <button id="btnLogin" lay-submit lay-filter="*" class="layui-btn">登錄</button>
            </div>
            <div class="form-message">
                <label th:text="${error}"></label>
                <label th:text="${msg}"></label>
            </div>
        </form>

防止跨站請求偽造(CSRF)攻擊

退出系統

<form id="logoutForm" action="/logout" method="post"
                                style="display: none;">
                                <input type="hidden" th:name="${_csrf.parameterName}"
                                    th:value="${_csrf.token}">
                            </form>
                            <a
                                href="javascript:document.getElementById('logoutForm').submit();">退出系統</a>

ajax 請求頁面

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" id="hidCSRF">
<button class="layui-btn" id="btnPublic">公共權限請求按鈕</button>
<br>
<br>
<button class="layui-btn" id="btnVip">VIP權限請求按鈕</button>
<script type="text/javascript" th:src="@{/js/jquery-1.8.3.min.js}"></script>
<script type="text/javascript" th:src="@{/layui/layui.js}"></script>
<script type="text/javascript">
        layui.use('form', function() {
            var form = layui.form;
            $("#btnPublic").click(function(){
                $.ajax({
                    url:"/test/public",
                    type:"POST",
                    data:{id:1},
                    beforeSend:function(xhr){
                        xhr.setRequestHeader('X-CSRF-TOKEN',$("#hidCSRF").val());   
                    },
                    success:function(res){
                        alert(res.code+":"+res.msg);
                
                    }   
                });
            });
            $("#btnVip").click(function(){
                $.ajax({
                    url:"/test/vip",
                    type:"POST",
                    data:{id:2},
                    beforeSend:function(xhr){
                        xhr.setRequestHeader('X-CSRF-TOKEN',$("#hidCSRF").val());   
                    },
                    success:function(res){
                        alert(res.code+":"+res.msg);
                        
                    }
                });
            });
        });
    </script>

2.8 測試

測試提供兩個賬號:user 和 admin (密碼與賬號一樣)

由於 admin 作為管理員權限,設置了全部的訪問權限,這裏只展示 user 的測試結果。

完整代碼

非特殊說明,本文版權歸 所有,轉載請註明出處.

原文標題:Spring Boot 2.X(十八):集成 Spring Security-登錄認證和權限控制

原文地址:

如果文章有不足的地方,歡迎提點,後續會完善。

如果文章對您有幫助,請給我點個贊,請掃碼關注下我的公眾號,文章持續更新中…

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

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

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

js 關於apply和call的理解使用

  關於call和apply,以前也思考良久,很多時候都以為記住了,但是,我太難了。今天我特地寫下筆記,希望可以完全掌握這個東西,也希望可以幫助到任何想對學習這個東西的同學。

一.apply函數定義與理解,先從apply函數出發

  在MDN上,apply的定義是:

    “apply()方法調用一個具有給定this值的函,以及作為一個數組(或)提供的參數。”

  我的理解是:apply的前面有個含有this的對象,設為A,apply()的參數里,也含有一個含有this的對象設為B。則A.apply(B),表示A代碼執行調用了B,B代碼照常執行,執行后的結果作為apply的參數,然後apply把這個結果所指代表示的this替換掉A本身的this,接着執行A代碼。

  比如:

 1     var aa = {
 2         _name:111,
 3         _age:222,
 4         _f:function(){
 5             console.log(this)
 6             console.log(this._name)
 7         }
 8     }
 9     var cc = {
10         _name:0,
11         _age:0,
12         _f:function(){
13             console.log(this)
14             console.log(this._name)
15         }
16     }
17     cc._f.apply(aa)//此時aa表示的this就是aa本身
18     cc._f.apply(aa._f)//此時aa._f表示的this就是aa._f本身
19     
20     /**
21      * 此時aa._f()表示的this,就是執行后的結果本身。aa._f()執行后,沒有返回值,所以應該是undefined,而undefined作為call和apply的參數時,call和apply前面的方法 cc._f 的this會替換成全局對象window。
22      * 參考MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/apply 的參數說明
23      */
24     cc._f.apply(aa._f())

執行結果:

  1.參數為aa

  

  這兩行的打印的都是來自 cc._f 方法內的那兩句console 。aa的時候算是初始化,裏面的 aa._f 方法沒有執行。

  2.參數為aa.f

  

  這兩行的打印的都是來自 cc._f 方法內的那兩句console 。aa.f 的時候應該也算是初始化,或者是整個函數當參數傳但是不執行這個參數,即 aa._f 方法沒有執行。

  3.參數為aa.f()

   

  這四行的打印,前面兩行來自 aa._f() 方法執行打印的;後面兩行是來自cc._f()方法打印的。

  后兩行解析:aa._f()執行后,沒有返回值,所以是undefined,在apply執行解析后,cc._f()的this變成的window,所以打印了window。window裏面沒有_name這個屬性,所以undefined。

 二.apply與call的區分

  1.apply()

    A.apply(B, [1,2,3]) 後面的參數是arguments對象或類似數組的對象,它會被自動解析為A的參數;

    對於A.apply(B) / A.call(B) , 簡單講,B先執行,執行后根據結果去執行A。這個時候,用A去執行B的內容代碼,然後再執行自己的代碼。

  比如:

    var f1 = function(a,b){
        console.log(a+b)
    }
    var f2 = function(a,b,c){
        console.log(a,b,c)
    }
    f2.apply(f1,[1,2])//1 2 undefined

   先執行f1,f1執行后(f1是f1,不是f1()執行方法,所以console.log(a+b)這行代碼並沒有執行,相當於,初始化了代碼f1),由於沒有返回值,所以結果是undefined,f2執行的時候this指向window;參數中的 ” [1,2] “,解析后變成 f2 的參數 “ 1,2,undefined ”,執行f2方法后,打印出1,2,undefined三個值

  2.call()

    A.call(B, 1,2,3) 後面的參數都是獨立的參數對象,它們會被自動解析為A的參數;

  比如: 

    var f1 = function(a,b){
        console.log(a+b)
    }
    var f2 = function(a,b,c){
        console.log(a,b,c)
    }
    f2.call(f1,[1,2])//[1,2] undefined undefined
    f2.call(f1,1,2)//1 2 undefined

   參數中的 ” [1,2] “,因為傳入了一個數組,相當於只傳入了第一個參數,b和c參數沒有傳。解析后變成 f2 的參數 “ [1,2],undefined ,undefined ”,執行f2方法后,打印出 [1,2],undefined ,undefined 三個值。

三.apply與call帶來的便利

  1. push();

  push參數是類似(a,b,c,d,e)如此傳輸的,如果在一個數組的基礎上進行傳輸另一個數組的內容,可以如下:

    //apply用法
    var arr = new Array(1,2,3)
    var arr1 = new Array(11,21,31)
    Array.prototype.push.apply(arr,arr1)
    console.log(arr)//[1, 2, 3, 11, 21, 31]
    
    //call用法
    var arr = new Array(1,2,3)
    var arr1 = new Array(11,21,31)
    Array.prototype.push.call(arr,arr1[0],arr1[1],arr1[2])
    console.log(arr)//[1, 2, 3, 11, 21, 31]

   2. 數組利用Math求最大和最小值

  apply和call的第一個參數,如果是null或者undefined,則apply或call前面的函數會把this指向window

    //apply的用法
    var _maxNum = Math.max.apply(null,[1,3,2,4,5])
    console.log(_maxNum)//5
    var _minNum = Math.min.apply(null,[1,3,2,4,5])
    console.log(_minNum)//1
    
    //call的用法
    var _maxNum = Math.max.call(null,1,3,2,4,5)
    console.log(_maxNum)//5
    var _minNum = Math.min.call(null,1,3,2,4,5)
    console.log(_minNum)//1

 四.總結

  簡而言之,apply和call函數的第一個參數都是用來替換this指向的對象;apply的第二個參數使用arguments或者類似數組的參數進行傳參,call的第二個或以上的參數,使用獨立單位,一個一個進行傳參;執行順序是apply或call的第一個參數先執行得到結果,然後執行apply或call前面的函數,執行的時候用已經執行的結果所指代的this去執行。apply和call的使用除了參數上的使用方式不同外,功能是一模一樣的。

  以上內容純屬個人理解,有誤勿噴請指出!謝謝!

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

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

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

jdbc-mysql測試例子和源碼詳解

目錄

簡介

什麼是JDBC

JDBC是一套連接和操作數據庫的標準、規範。通過提供DriverManagerConnectionStatementResultSet等接口將開發人員與數據庫提供商隔離,開發人員只需要面對JDBC接口,無需關心怎麼跟數據庫交互。

幾個重要的類

類名 作用
DriverManager 驅動管理器,用於註冊驅動,是獲取 Connection對象的入口
Driver 數據庫驅動,用於獲取Connection對象
Connection 數據庫連接,用於獲取Statement對象、管理事務
Statement sql執行器,用於執行sql
ResultSet 結果集,用於封裝和操作查詢結果
prepareCall 用於調用存儲過程

使用中的注意事項

  1. 記得釋放資源。另外,ResultSetStatement的關閉都不會導致Connection的關閉。

  2. maven要引入oracle的驅動包,要把jar包安裝在本地倉庫或私服才行。

  3. 使用PreparedStatement而不是Statement。可以避免SQL注入,並且利用預編譯的特點可以提高效率。

使用例子

需求

使用JDBC對mysql數據庫的用戶表進行增刪改查。

工程環境

JDK:1.8

maven:3.6.1

IDE:sts4

mysql driver:8.0.15

mysql:5.7

主要步驟

一個完整的JDBC保存操作主要包括以下步驟:

  1. 註冊驅動(JDK6後會自動註冊,可忽略該步驟);

  2. 通過DriverManager獲得Connection對象;

  3. 開啟事務;

  4. 通過Connection獲得PreparedStatement對象;

  5. 設置PreparedStatement的參數;

  6. 執行保存操作;

  7. 保存成功提交事務,保存失敗回滾事務;

  8. 釋放資源,包括ConnectionPreparedStatement

創建表

CREATE TABLE `demo_user` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '用戶id',
  `name` varchar(16) COLLATE utf8_unicode_ci NOT NULL COMMENT '用戶名',
  `age` int(3) unsigned DEFAULT NULL COMMENT '用戶年齡',
  `gmt_create` datetime DEFAULT NULL COMMENT '記錄創建時間',
  `gmt_modified` datetime DEFAULT NULL COMMENT '記錄最近修改時間',
  `deleted` bit(1) DEFAULT b'0' COMMENT '是否刪除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_name` (`name`),
  KEY `index_age` (`age`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

創建項目

項目類型Maven Project,打包方式jar

引入依賴

<!-- junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<!-- mysql驅動的jar包 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.15</version>
</dependency>
<!-- oracle驅動的jar包 -->
<!-- <dependency>
    <groupId>com.oracle</groupId>
    <artifactId>ojdbc6</artifactId>
    <version>11.2.0.2.0</version>
</dependency> -->

注意:由於oracle商業版權問題,maven並不提供Oracle JDBC driver,需要將驅動包手動添加到本地倉庫或私服。

編寫jdbc.prperties

下面的url拼接了好幾個參數,主要為了避免亂碼和時區報錯的異常。

路徑:resources目錄下

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
#這裏指定了字符編碼和解碼格式,時區,是否加密傳輸
username=root
password=root
#注意,xml配置是&採用&amp;替代

如果是oracle數據庫,配置如下:

driver=oracle.jdbc.driver.OracleDriver
url=jdbc:oracle:thin:@//localhost:1521/xe
username=system
password=root

獲得Connection對象

    private static Connection createConnection() throws Exception {
        // 導入配置文件
        Properties pro = new Properties();
        InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream( "jdbc.properties" );
        Connection conn = null;
        pro.load( in );
        // 獲取配置文件的信息
        String driver = pro.getProperty( "driver" );
        String url = pro.getProperty( "url" );
        String username = pro.getProperty( "username" );
        String password = pro.getProperty( "password" );
        // 註冊驅動,JDK6后不需要再手動註冊,DirverManager的靜態代碼塊會幫我們註冊
        // Class.forName(driver);
        // 獲得連接
        conn = DriverManager.getConnection( url, username, password );
        return conn;
    }

使用Connection對象完成保存操作

這裏簡單地模擬實際業務層調用持久層,並開啟事務。另外,獲取連接、開啟事務、提交回滾、釋放資源都通過自定義的工具類 JDBCUtil 來實現,具體見源碼。

    @Test
    public void save() {
        UserDao userDao = new UserDaoImpl();
        // 創建用戶
        User user = new User( "zzf002", 18, new Date(), new Date() );
        try {
            // 開啟事務
            JDBCUtil.startTrasaction();
            // 保存用戶
            userDao.insert( user );
            // 提交事務
            JDBCUtil.commit();
        } catch( Exception e ) {
            // 回滾事務
            JDBCUtil.rollback();
            e.printStackTrace();
        } finally {
            // 釋放資源
            JDBCUtil.release();
        }
    }

接下來看看具體的保存操作,即DAO層方法。

    public void insert( User user ) throws Exception {
        String sql = "insert into demo_user (name,age,gmt_create,gmt_modified) values(?,?,?,?)";
        Connection connection = JDBCUtil.getConnection();
        //獲取PreparedStatement對象
        PreparedStatement prepareStatement = connection.prepareStatement( sql );
        //設置參數
        prepareStatement.setString( 1, user.getName() );
        prepareStatement.setInt( 2, user.getAge() );
        prepareStatement.setDate( 3, new java.sql.Date( user.getGmt_create().getTime() ) );
        prepareStatement.setDate( 4, new java.sql.Date( user.getGmt_modified().getTime() ) );
        //執行保存
        prepareStatement.executeUpdate();
        //釋放資源
        JDBCUtil.release( prepareStatement, null );
    }

源碼分析

驅動註冊

DriverManager.registerDriver

DriverManager主要用於管理數據庫驅動,併為我們提供了獲取連接對象的接口。其中,它有一個重要的成員屬性registeredDrivers,是一個CopyOnWriteArrayList集合(通過ReentrantLock實現線程安全),存放的是元素是DriverInfo對象。

    //存放數據庫驅動包裝類的集合(線程安全)
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); 
    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {
        //調用重載方法,傳入的DriverAction對象為null
        registerDriver(driver, null);
    }
    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {
        if(driver != null) {
            //當列表中沒有這個DriverInfo對象時,加入列表。
            //注意,這裏判斷對象是否已經存在,最終比較的是driver地址是否相等。
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

    }

為什麼集合存放的是Driver的包裝類DriverInfo對象,而不是Driver對象呢?

  1. 通過DriverInfo的源碼可知,當我們調用equals方法比較兩個DriverInfo對象是否相等時,實際上比較的是Driver對象的地址,也就是說,我可以在DriverManager中註冊多個MYSQL驅動。而如果直接存放的是Driver對象,就不能達到這種效果(因為沒有遇到需要註冊多個同類驅動的場景,所以我暫時理解不了這樣做的好處)。

  2. DriverInfo中還包含了另一個成員屬性DriverAction,當我們註銷驅動時,必須調用它的deregister方法后才能將驅動從註冊列表中移除,該方法決定註銷驅動時應該如何處理活動連接等(其實一般在構造DriverInfo進行註冊時,傳入的DriverAction對象為空,根本不會去使用到這個對象,除非一開始註冊就傳入非空DriverAction對象)。

綜上,集合中元素不是Driver對象而DriverInfo對象,主要考慮的是擴展某些功能,雖然這些功能幾乎不會用到。

注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。

class DriverInfo {

    final Driver driver;
    DriverAction da;
    DriverInfo(Driver driver, DriverAction action) {
        this.driver = driver;
        da = action;
    }

    @Override
    public boolean equals(Object other) {
        //這裏對比的是地址
        return (other instanceof DriverInfo)
                && this.driver == ((DriverInfo) other).driver;
    }

}

為什麼Class.forName(com.mysql.cj.jdbc.Driver) 可以註冊驅動?

當加載com.mysql.cj.jdbc.Driver這個類時,靜態代碼塊中會執行註冊驅動的方法。

    static {
        try {
            //靜態代碼塊中註冊當前驅動
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

為什麼JDK6后不需要Class.forName也能註冊驅動?

因為從JDK6開始,DriverManager增加了以下靜態代碼塊,當類被加載時會執行static代碼塊的loadInitialDrivers方法。

而這個方法會通過查詢系統參數(jdbc.drivers)SPI機制兩種方式去加載數據庫驅動。

注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。

    static {
        loadInitialDrivers();
    }
    //這個方法通過兩個渠道加載所有數據庫驅動:
    //1. 查詢系統參數jdbc.drivers獲得數據驅動類名
    //2. SPI機制
    private static void loadInitialDrivers() {
        //通過系統參數jdbc.drivers讀取數據庫驅動的全路徑名。該參數可以通過啟動參數來設置,其實引入SPI機制后這一步好像沒什麼意義了。
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        //使用SPI機制加載驅動
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                //讀取META-INF/services/java.sql.Driver文件的類全路徑名。
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                //加載並初始化類
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        if (drivers == null || drivers.equals("")) {
            return;
        }
        //加載jdbc.drivers參數配置的實現類
        String[] driversList = drivers.split(":");
        for (String aDriver : driversList) {
            try {
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

補充:SPI機制本質上提供了一種服務發現機制,通過配置文件的方式,實現服務的自動裝載,有利於解耦和面向接口編程。具體實現過程為:在項目的META-INF/services文件夾下放入以接口全路徑名命名的文件,並在文件中加入實現類的全限定名,接着就可以通過ServiceLoder動態地加載實現類。

打開mysql的驅動包就可以看到一個java.sql.Driver文件,裏面就是mysql驅動的全路徑名。

獲得連接對象

DriverManager.getConnection

獲取連接對象的入口是DriverManager.getConnection,調用時需要傳入url、username和password。

獲取連接對象需要調用java.sql.Driver實現類(即數據庫驅動)的方法,而具體調用哪個實現類呢?

正如前面講到的,註冊的數據庫驅動被存放在registeredDrivers中,所以只有從這個集合中獲取就可以了。

注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。

    public static Connection getConnection(String url, String user, String password) throws SQLException {
        java.util.Properties info = new java.util.Properties();

        if (user != null) {
            info.put("user", user);
        }
        if (password != null) {
            info.put("password", password);
        }
        //傳入url、包含username和password的信息類、當前調用類
        return (getConnection(url, info, Reflection.getCallerClass()));
    }
    private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        //遍歷所有註冊的數據庫驅動
        for(DriverInfo aDriver : registeredDrivers) {
            //先檢查這當前類加載器是否有權限加載這個驅動,如果是才進入
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                //這一步是關鍵,會去調用Driver的connect方法
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
                    return con;
                }
            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }
        }
    }

com.mysql.cj.jdbc.Driver.connection

由於使用的是mysql的數據驅動,這裏實際調用的是com.mysql.cj.jdbc.Driver的方法。

從以下代碼可以看出,mysql支持支持多節點部署的策略,本文僅對單機版進行擴展。

注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。

    //mysql支持多節點部署的策略,根據架構不同,url格式也有所區別。
    private static final String REPLICATION_URL_PREFIX = "jdbc:mysql:replication://";
    private static final String URL_PREFIX = "jdbc:mysql://";
    private static final String MXJ_URL_PREFIX = "jdbc:mysql:mxj://";
    public static final String LOADBALANCE_URL_PREFIX = "jdbc:mysql:loadbalance://";
    public java.sql.Connection connect(String url, Properties info) throws SQLException {
        //根據url的類型來返回不同的連接對象,這裏僅考慮單機版
        ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
        switch (conStr.getType()) {
            case SINGLE_CONNECTION:
                //調用ConnectionImpl.getInstance獲取連接對象
                return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());

            case LOADBALANCE_CONNECTION:
                return LoadBalancedConnectionProxy.createProxyInstance((LoadbalanceConnectionUrl) conStr);

            case FAILOVER_CONNECTION:
                return FailoverConnectionProxy.createProxyInstance(conStr);

            case REPLICATION_CONNECTION:
                return ReplicationConnectionProxy.createProxyInstance((ReplicationConnectionUrl) conStr);

            default:
                return null;
        }
    }

ConnectionImpl.getInstance

這個類有個比較重要的字段session,可以把它看成一個會話,和我們平時瀏覽器訪問服務器的會話差不多,後續我們進行數據庫操作就是基於這個會話來實現的。

注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。

    private NativeSession session = null;
    public static JdbcConnection getInstance(HostInfo hostInfo) throws SQLException {
        //調用構造
        return new ConnectionImpl(hostInfo);
    }
    public ConnectionImpl(HostInfo hostInfo) throws SQLException {
        //先根據hostInfo初始化成員屬性,包括數據庫主機名、端口、用戶名、密碼、數據庫及其他參數設置等等,這裏省略不放入。
        //最主要看下這句代碼 
        createNewIO(false);
    }
    public void createNewIO(boolean isForReconnect) {
        if (!this.autoReconnect.getValue()) {
            //這裏只看不重試的方法
            connectOneTryOnly(isForReconnect);
            return;
        }

        connectWithRetries(isForReconnect);
    }
    private void connectOneTryOnly(boolean isForReconnect) throws SQLException {

        JdbcConnection c = getProxy();
        //調用NativeSession對象的connect方法建立和數據庫的連接
        this.session.connect(this.origHostInfo, this.user, this.password, this.database, DriverManager.getLoginTimeout() * 1000, c);
        return;
    }

NativeSession.connect

接下來的代碼主要是建立會話的過程,首先時建立物理連接,然後根據協議建立會話。

注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。

    public void connect(HostInfo hi, String user, String password, String database, int loginTimeout, TransactionEventHandler transactionManager)
            throws IOException {
        //首先獲得TCP/IP連接
        SocketConnection socketConnection = new NativeSocketConnection();
        socketConnection.connect(this.hostInfo.getHost(), this.hostInfo.getPort(), this.propertySet, getExceptionInterceptor(), this.log, loginTimeout);

        // 對TCP/IP連接進行協議包裝
        if (this.protocol == null) {
            this.protocol = NativeProtocol.getInstance(this, socketConnection, this.propertySet, this.log, transactionManager);
        } else {
            this.protocol.init(this, socketConnection, this.propertySet, transactionManager);
        }

        // 通過用戶名和密碼連接指定數據庫,並創建會話
        this.protocol.connect(user, password, database);
    }

針對數據庫的連接,暫時點到為止,另外還有涉及數據庫操作的源碼分析,後續再完善補充。

本文為原創文章,轉載請附上原文出處鏈接:https://github.com/ZhangZiSheng001/jdbc-demo

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

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

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

[FPGA]淺談LCD1602字符型液晶显示器(Verilog)

概述

本文圍繞LCD1602字符型液晶显示器展開,並在FPGA開發板上用VerilogHDL語言實現模塊驅動.

首先來一張效果展示

那麼怎麼在這塊綠油油的平面上显示出點陣構成的字符呢?本文將為你提供一些思路.

注:本文僅討論寫入操作,實現在LCD1602上显示指定字符串,不講解讀取相關操作.

LCD1602

LCD1602是什麼?

LCD1602是一種字符型液晶显示模塊,不同於七段數碼管,它可以通過點陣的形式显示出各種圖案或字符,可拓展性較強.

其名稱中”LCD”即為 Liquid Crystal Display (液晶显示器),”1602″代表显示屏上可同時显示32個字符(16×2).

LCD1602的管腳

LCD1602共有16根管腳(部分型號只有14根,沒有背光管腳),管腳功能表如下

符號 管腳說明 符號 管腳說明
VSS 電源地 D2 數據
VDD 電源正極 D3 數據
VL 偏壓 D4 數據
RS 數據/命令選擇 D5 數據
R/W 讀/寫選擇 D6 數據
E 使能 D7 數據
D0 數據 BLA 背光正極
D1 數據 BLK 背光負極

其中需要我們關心的只有RS,E,和D0-D7.

RS_數據/命令選擇

RS端用來控制輸入給D0-D7的序列代表命令還是數據.

如果代表輸入命令,則輸入給D0-D7的序列相當於對模塊進行設置(下文會有輸入序列對應的指令表及其功能);如果代表輸入數據,則輸入給D0-D7的序列相當於寫入需要显示的字符串(輸入的是每個字符所對應的地址碼).

若RS為低電平,代表輸入命令;若RS為高電平,代表輸入數據.

E_使能

E端是用來執行命令的使能引腳,當它從高電平變成低電平時(下降沿),液晶模塊執行命令.

D0-D7

八位雙向并行數據線,在本文中僅作輸入端(寫入).

LCD1602有個DDRAM

DDRAM( Display Data Random Access Memory )即為显示數據隨機存取存儲器,相當於”顯存”,用來存放待显示的字符代碼.

DDRAM一共有80個字節,它和1602的显示屏上32個字符位的對應地址如下圖

第一行的16個字符位的地址對應0x00-0x0F,第二行則對應0x40-0x4F(“0x”代表16進制數).

LCD1602還有個CGROM

CGROM( Character Generator Read-Only Memory )即為字符產生只讀存儲器,用來存放192個常用字符的字模.

值得一提的是,表中的左半部分字符和他們的ASCII碼是對應的,所以在寫代碼時可以直接寫成”A”而不必要寫成”0x41″.

另外還有一個CGRAM用來存放用戶自定義的字符,可存放8個5×8字符或4個5×10字符,不過這不在本文討論範圍內.

指令集

前文已經提到,當RS為低電平時,代表輸入命令,那麼這些命令都有哪些呢?

將能實現某種功能的序列稱為一條命令,每條命令有幾個固定的位和幾個可變的位,可變的位可以改變功能/模式,將這些命令總稱為指令集.全體指令集如下錶

指令 RS R/W D7 D6 D5 D4 D3 D2 D1 D0
清屏 0 0 0 0 0 0 0 0 0 1
光標複位 0 0 0 0 0 0 0 0 0 x
進入模式設置 0 0 0 0 0 0 0 1 I/D S
显示開關設置 0 0 0 0 0 0 1 D C B
移位控制 0 0 0 0 0 1 S/C R/L x x
工作方式設置 0 0 0 0 1 DL N F x x
字符發生器地址設置 0 0 0 1 a a a a a a
數據存儲器地址設置 0 0 1 b b b b b b b
讀忙標誌或地址 0 1 BF c c c c c c c
寫入數據至CDRAM或DDRAM 1 0 d d d d d d d d
從CGRAM或DDRAM中讀取數據 1 1 e e e e e e e e

注:其中a代表字符發生存儲器地址,b代表显示數據存儲器地址,c代表計數器地址,d代表要寫入的數據內容,e代表讀取的數據內容.

我們關心的是其中的清屏,進入模式設置,显示開關設置,工作方式設置,數據存儲器地址設置.

清屏

清除屏幕显示內容,光標返回屏幕左上角.

執行這個指令時需要一定時間.

進入模式設置

I/D = 1:寫入新數據后光標右移,I/D = 0:寫入新數據后光標左移

S = 1:显示移動,S = 0:显示不移動.

显示開關設置

D = 1:显示功能開,D = 0,显示功能關(但是DDRAM中的數據依然保留).

C = 1:有光標,C = 0,沒有光標.

B = 1:光標閃爍,B = 0.光標不閃爍.

工作方式設置

DL = 1:8位數據接口(D7-D0),DL = 0:4位數據接口(D7-D4).

N = 0:一行显示,N = 1;兩行显示.

F = 0: 5×8點陣字符,F = 1: 5×10點陣字符.

數據存儲器地址設置

在對DDRAM進行讀寫之前,首先要設置DDRAM地址,然後才能進行讀寫.

地址設置見.

Verilog驅動

了解了1602的原理和功能后,就可以着手編寫驅動模塊了.想要讓LCD1602显示指定的字符,需要有一個驅動程序將模塊和用戶連接起來,實現輸入什麼就輸出什麼的功能,並能夠簡單的進行設置.

接下來開始寫驅動(造輪子).分為若干個次級模塊逐個分析.

模塊定義

模塊共有5個端口(其中8個數據端合為一個8位寬端口),分別為CLK時鐘輸入端,_RST低電平有效的複位端,LCD_E使能端,LCD_RS數據/命令選擇端,LCD_DATA數據端.

module LCD1602
(input CLK
,input _RST
,output LCD_E 
,output reg LCD_RS
,output reg[7:0]LCD_DATA
);

上電穩定

這是一個簡單的初始化模塊,數據手冊要求要先通電20ms才可以進行下一步操作,為了使之上電穩定.

parameter TIME_20MS=1_000_000;//需要20ms以達上電穩定(初始化)
reg[19:0]cnt_20ms;
always@(posedge CLK or negedge _RST)
    if(!_RST)
        cnt_20ms<=1'b0;
    else if(cnt_20ms==TIME_20MS-1'b1)
        cnt_20ms<=cnt_20ms;
    else
        cnt_20ms<=cnt_20ms+1'b1 ;

wire delay_done=(cnt_20ms==TIME_20MS-1'b1)?1'b1:1'b0;//上電延時完畢

工作周期分頻

LCD1602的工作周期為500Hz,所以要進行分頻(板載晶振為50MHz).

parameter TIME_500HZ=100_000;//工作周期
reg[19:0]cnt_500hz;
always@(posedge CLK or negedge _RST)
    if(!_RST)
        cnt_500hz<=1'b0;
    else if(delay_done)
        if(cnt_500hz==TIME_500HZ-1'b1)
            cnt_500hz<=1'b0;
        else
            cnt_500hz<=cnt_500hz+1'b1;
    else
        cnt_500hz<=1'b0;

assign LCD_E=(cnt_500hz>(TIME_500HZ-1'b1)/2)?1'b0:1'b1;//使能端,每個工作周期一次下降沿,執行一次命令
wire write_flag=(cnt_500hz==TIME_500HZ-1'b1)?1'b1:1'b0;//每到一個工作周期,write_flag置高一周期

狀態機

模塊工作採用狀態機驅動.

//狀態機有40種狀態,此處用了格雷碼,一次只有一位變化(在二進制下)
parameter IDLE=8'h00;
parameter SET_FUNCTION=8'h01;
parameter DISP_OFF=8'h03;
parameter DISP_CLEAR=8'h02;
parameter ENTRY_MODE=8'h06;
parameter DISP_ON=8'h07;
parameter ROW1_ADDR=8'h05;
parameter ROW1_0=8'h04;
parameter ROW1_1=8'h0C;
parameter ROW1_2=8'h0D;
parameter ROW1_3=8'h0F;
parameter ROW1_4=8'h0E;
parameter ROW1_5=8'h0A;
parameter ROW1_6=8'h0B;
parameter ROW1_7=8'h09;
parameter ROW1_8=8'h08;
parameter ROW1_9=8'h18;
parameter ROW1_A=8'h19;
parameter ROW1_B=8'h1B;
parameter ROW1_C=8'h1A;
parameter ROW1_D=8'h1E;
parameter ROW1_E=8'h1F;
parameter ROW1_F=8'h1D;
parameter ROW2_ADDR=8'h1C;
parameter ROW2_0=8'h14;
parameter ROW2_1=8'h15;
parameter ROW2_2=8'h17;
parameter ROW2_3=8'h16;
parameter ROW2_4=8'h12;
parameter ROW2_5=8'h13;
parameter ROW2_6=8'h11;
parameter ROW2_7=8'h10;
parameter ROW2_8=8'h30;
parameter ROW2_9=8'h31;
parameter ROW2_A=8'h33;
parameter ROW2_B=8'h32;
parameter ROW2_C=8'h36;
parameter ROW2_D=8'h37;
parameter ROW2_E=8'h35;
parameter ROW2_F=8'h34;

reg[5:0]c_state;//Current state,當前狀態
reg[5:0]n_state;//Next state,下一狀態

always@(posedge CLK or negedge _RST)
    if(!_RST)
        c_state<=IDLE;
    else if(write_flag)//每一個工作周期改變一次狀態
        c_state<=n_state;
    else
        c_state<=c_state;

always@(*)
    case (c_state)
        IDLE:n_state=SET_FUNCTION;
        SET_FUNCTION:n_state=DISP_OFF;
        DISP_OFF:n_state=DISP_CLEAR;
        DISP_CLEAR:n_state=ENTRY_MODE;
        ENTRY_MODE:n_state=DISP_ON;
        DISP_ON:n_state=ROW1_ADDR;
        ROW1_ADDR:n_state=ROW1_0;
        ROW1_0:n_state=ROW1_1;
        ROW1_1:n_state=ROW1_2;
        ROW1_2:n_state=ROW1_3;
        ROW1_3:n_state=ROW1_4;
        ROW1_4:n_state=ROW1_5;
        ROW1_5:n_state=ROW1_6;
        ROW1_6:n_state=ROW1_7;
        ROW1_7:n_state=ROW1_8;
        ROW1_8:n_state=ROW1_9;
        ROW1_9:n_state=ROW1_A;
        ROW1_A:n_state=ROW1_B;
        ROW1_B:n_state=ROW1_C;
        ROW1_C:n_state=ROW1_D;
        ROW1_D:n_state=ROW1_E;
        ROW1_E:n_state=ROW1_F;
        ROW1_F:n_state=ROW2_ADDR;
        ROW2_ADDR:n_state=ROW2_0;
        ROW2_0:n_state=ROW2_1;
        ROW2_1:n_state=ROW2_2;
        ROW2_2:n_state=ROW2_3;
        ROW2_3:n_state=ROW2_4;
        ROW2_4:n_state=ROW2_5;
        ROW2_5:n_state=ROW2_6;
        ROW2_6:n_state=ROW2_7;
        ROW2_7:n_state=ROW2_8;
        ROW2_8:n_state=ROW2_9;
        ROW2_9:n_state=ROW2_A;
        ROW2_A:n_state=ROW2_B;
        ROW2_B:n_state=ROW2_C;
        ROW2_C:n_state=ROW2_D;
        ROW2_D:n_state=ROW2_E;
        ROW2_E:n_state=ROW2_F;
        ROW2_F:n_state=ROW1_ADDR;//循環到1-1進行掃描显示
        default:;
    endcase

RS端控制

控制輸入為數據或命令

always@(posedge CLK or negedge _RST)
    if(!_RST)
        LCD_RS<=1'b0;//為0時輸入指令,為1時輸入數據
    else if(write_flag)
        //當狀態為七個指令任意一個,將RS置為指令輸入狀態
        if((n_state==SET_FUNCTION)||(n_state==DISP_OFF)||(n_state==DISP_CLEAR)||(n_state==ENTRY_MODE)||(n_state==DISP_ON)||(n_state==ROW1_ADDR)||(n_state==ROW2_ADDR))
            LCD_RS<=1'b0; 
        else
            LCD_RS<=1'b1;
    else
        LCD_RS<=LCD_RS;

显示控制

always@(posedge CLK or negedge _RST)
    if(!_RST)
        LCD_DATA<=1'b0;
    else if(write_flag)
        case(n_state)
            IDLE:LCD_DATA<=8'hxx;
            SET_FUNCTION:LCD_DATA<=8'h38;//8'b0011_1000,工作方式設置:DL=1(DB4,8位數據接口),N=1(DB3,兩行显示),L=0(DB2,5x8點陣显示).
            DISP_OFF:LCD_DATA<=8'h08;//8'b0000_1000,显示開關設置:D=0(DB2,显示關),C=0(DB1,光標不显示),D=0(DB0,光標不閃爍)
            DISP_CLEAR:LCD_DATA<=8'h01;//8'b0000_0001,清屏
            ENTRY_MODE:LCD_DATA<=8'h06;//8'b0000_0110,進入模式設置:I/D=1(DB1,寫入新數據光標右移),S=0(DB0,显示不移動)
            DISP_ON:LCD_DATA<=8'h0c;//8'b0000_1100,显示開關設置:D=1(DB2,显示開),C=0(DB1,光標不显示),D=0(DB0,光標不閃爍)
            ROW1_ADDR:LCD_DATA<=8'h80;//8'b1000_0000,設置DDRAM地址:00H->1-1,第一行第一位
            //將輸入的row_1以每8-bit拆分,分配給對應的显示位
            ROW1_0:LCD_DATA<=row_1[127:120];
            ROW1_1:LCD_DATA<=row_1[119:112];
            ROW1_2:LCD_DATA<=row_1[111:104];
            ROW1_3:LCD_DATA<=row_1[103: 96];
            ROW1_4:LCD_DATA<=row_1[ 95: 88];
            ROW1_5:LCD_DATA<=row_1[ 87: 80];
            ROW1_6:LCD_DATA<=row_1[ 79: 72];
            ROW1_7:LCD_DATA<=row_1[ 71: 64];
            ROW1_8:LCD_DATA<=row_1[ 63: 56];
            ROW1_9:LCD_DATA<=row_1[ 55: 48];
            ROW1_A:LCD_DATA<=row_1[ 47: 40];
            ROW1_B:LCD_DATA<=row_1[ 39: 32];
            ROW1_C:LCD_DATA<=row_1[ 31: 24];
            ROW1_D:LCD_DATA<=row_1[ 23: 16];
            ROW1_E:LCD_DATA<=row_1[ 15:  8];
            ROW1_F:LCD_DATA<=row_1[  7:  0];
            ROW2_ADDR:LCD_DATA<=8'hc0;//8'b1100_0000,設置DDRAM地址:40H->2-1,第二行第一位
            ROW2_0:LCD_DATA<=row_2[127:120];
            ROW2_1:LCD_DATA<=row_2[119:112];
            ROW2_2:LCD_DATA<=row_2[111:104];
            ROW2_3:LCD_DATA<=row_2[103: 96];
            ROW2_4:LCD_DATA<=row_2[ 95: 88];
            ROW2_5:LCD_DATA<=row_2[ 87: 80];
            ROW2_6:LCD_DATA<=row_2[ 79: 72];
            ROW2_7:LCD_DATA<=row_2[ 71: 64];
            ROW2_8:LCD_DATA<=row_2[ 63: 56];
            ROW2_9:LCD_DATA<=row_2[ 55: 48];
            ROW2_A:LCD_DATA<=row_2[ 47: 40];
            ROW2_B:LCD_DATA<=row_2[ 39: 32];
            ROW2_C:LCD_DATA<=row_2[ 31: 24];
            ROW2_D:LCD_DATA<=row_2[ 23: 16];
            ROW2_E:LCD_DATA<=row_2[ 15:  8];
            ROW2_F:LCD_DATA<=row_2[  7:  0];
        endcase
    else
        LCD_DATA<=LCD_DATA;

自定義字符輸入

輸入要显示的字符.

wire[127:0]row_1;
wire[127:0]row_2;
assign row_1 ="   Welcome to   ";//第一行显示的內容(16個字符)
assign row_2 ="    My Blog!    ";//第二行显示的內容(16個字符)

效果展示

將以上代碼有機整合后,燒錄至開發板上,按下複位鍵即可看到显示屏上显示出了指定字樣.

你可以修改字符串來讓屏幕显示出不同的內容,甚至可以調整模式讓显示屏滾動显示大於16字符的字符串.

總結

LCD1602是一個很基礎的模塊,把這個掌握后對以後的學習幫助很大,所以很有必要學習.

這個模塊不止可以通過Verilog驅動,也可以用其他語言或其他開發板來實現,例如STM32,51單片機或者SV,VHDL語言,都可以寫一套讓他工作的驅動.

另外,如果有現成的輪子,為什麼還要自己造一個出來呢?在碰到類似情況時可以藉助互聯網參考一下別人對此問題有怎麼樣的解決方案,加以借鑒並內化於心,才能達到最高效率的學習.

參考資料

[1] aslmer. “verilog寫的LCD1602 显示”[ED/OL]. https://www.cnblogs.com/aslmer/p/5819422.html ,2016(8).

[2] aslmer. “LCD1602指令集解讀”[ED/OL]. https://www.cnblogs.com/aslmer/p/5801363.html ,2016(8).

[3] 阿忠ZHONG. “單片機显示原理(LCD1602)”[ED/OL]. https://www.cnblogs.com/hui088/p/4732034.html 2015(8).

[4] 百度百科. “詞條-LCD1602″[ED/OL]. https://baike.baidu.com/item/LCD1602/6014393 ,2019(9).

[5] HITACHI©Ltd. “HD44780U (LCD-II)(Dot Matrix Liquid Crystal Display Controller/Driver)”[M]. Japan HITACHI,1998.

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

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

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

http協議詳細介紹

1.1 HTTP協議簡介
我們日常生活中經常會使用瀏覽器訪問Web站點,但是大家有思考過在這個過程中到底發生了什麼嗎?為什麼我們在瀏覽器地址欄上面輸入要訪問的URL后就可以訪問到Web頁面呢?

1.1.1 瀏覽器背後的故事
當我們在瀏覽器地址欄上輸入要訪問的URL后,瀏覽器會分析出URL上面的域名,然後通過DNS服務器查詢出域名映射的IP地址,瀏覽器根據查詢到的IP地址與Web服務器進行通信,而通信的協議就是HTTP協議。

我們可以把這個過程類比成一個電話對話的過程。當我們要打電話給某個人,首先要知道對方的電話號碼,然後進行撥號。打通電話后我們會進行對話,當然要對話肯定需要共同的語言,如果一個人說國語,而另一個人說英語,那肯定不能進行溝通的。在本例中,電話號碼相當於上面的IP地址,而共同語言相當於HTTP協議。

我們通過一個簡單的圖來闡述這個過程:

瀏覽器與Web服務器使用HTTP協議進行通信,那麼什麼是HTTP協議呢?接下來我們會詳細介紹HTTP協議的相關知識。

1.1.2 TCP/IP協議
HTTP協議是構建在TCP/IP協議之上的,是TCP/IP協議的一個子集,所以要理解HTTP協議,有必要先了解下TCP/IP協議相關的知識。

由於TCP/IP協議族包含眾多的協議,在這裏我們無法一一討論。接下來,我們僅介紹理解HTTP協議需要掌握的TCP/IP協議族的一些相關知識點。如果想深入理解TCP/IP協議,可以參考經典書籍《TCP/IP詳解》。

TCP/IP協議族分層

TCP/IP協議族是由一個四層協議組成的系統,這四層分別為:應用層、傳輸層、網絡層和數據鏈路層。如圖所示

分層的好處是把各個相對獨立的功能解耦,層與層之間通過規定好的接口來通信。如果以後需要修改或者重寫某一個層的實現,只要接口保持不變也不會影響到其他層的功能。接下來,我們將會介紹各個層的主要作用。

1) 應用層

應用層一般是我們編寫的應用程序,其決定了向用戶提供的應用服務。應用層可以通過系統調用與傳輸層進行通信。

處於應用層的協議非常多,比如:FTP(File Transfer Protocol,文件傳輸協議)、DNS(Domain Name System,域名系統)和我們本章討論的HTTP(HyperText Transfer Protocol,超文本傳輸協議)等。

2) 傳輸層

傳輸層通過系統調用嚮應用層提供處於網絡連接中的兩台計算機之間的數據傳輸功能。

在傳輸層有兩個性質不同的協議:TCP(Transmission Control Protocol,傳輸控制協議)和UDP(User Data Protocol,用戶數據報協議)。

3) 網絡層

網絡層用來處理在網絡上流動的數據包,數據包是網絡傳輸的最小數據單位。該層規定了通過怎樣的路徑(傳輸路線)到達對方計算機,並把數據包傳輸給對方。

4) 鏈路層

鏈路層用來處理連接網絡的硬件部分,包括控制操作系統、硬件設備驅動、NIC(Network Interface Card,網絡適配器)以及光纖等物理可見部分。硬件上的範疇均在鏈路層的作用範圍之內。

數據包封裝

上層協議數據是如何轉變為下層協議數據的呢?這是通過封裝(encapsulate)來實現的。應用程序數據在發送到物理網絡之前,會沿着協議棧從上往下傳遞。每層協議都將在上層協議數據的基礎上加上自己的頭部信息(鏈路層還會加上尾部信息),以為實現該層功能提供必要的信息。如圖所示:

發送端發送數據時,數據會從上層傳輸到下層,且每經過一層都會被打上該層的頭部信息。而接收端接收數據時,數據會從下層傳輸到上層,傳輸前會把下層的頭部信息刪除。過程如圖所示:

數據傳輸過程

由於下層協議的頭部信息對上層協議是沒有實際的用途,所以在下層協議傳輸數據給上層協議的時候會把該層的頭部信息去掉,這個封裝過程對於上層協議來說是完全透明的。這樣做的好處是,應用層只需要關心應用服務的實現,而不用管底層的實現。

TCP三次握手

從上面的介紹可知,傳輸層協議主要有兩個:TCP協議和UDP協議。TCP協議相對於UDP協議的特點是:TCP協議提供面向連接、字節流和可靠的傳輸。

使用TCP協議進行通信的雙方必須先建立連接,然後才能開始傳輸數據。TCP連接是全雙工的,也就是說雙方的數據讀寫可以通過一個連接進行。為了確保連接雙方可靠性,在雙方建立連接時,TCP協議採用了三次握手(Three-way handshaking)策略。
過程如圖所示:

TCP協議三次握手的描述如下:

第一次握手:客戶端發送帶有SYN標誌的連接請求報文段,然後進入SYN_SEND狀態,等待服務端的確認。

第二次握手:服務端接收到客戶端的SYN報文段后,需要發送ACK信息對這個SYN報文段進行確認。同時,還要發送自己的SYN請求信息。服務端會將上述的信息放到一個報文段(SYN+ACK報文段)中,一併發送給客戶端,此時服務端將會進入SYN_RECV狀態。

第三次握手:客戶端接收到服務端的SYN+ACK報文段后,會想服務端發送ACK確認報文段,這個報文段發送完畢后,客戶端和服務端都進入ESTABLISHED狀態,完成TCP三次握手。

當三次握手完成后,TCP協議會為連接雙方維持連接狀態。為了保證數據傳輸成功,接收端在接收到數據包后必須發送ACK報文作為確認。如果在指定的時間內(這個時間稱為重新發送超時時間),發送端沒有接收到接收端的ACK報文,那麼就會重發超時的數據。

1.1.3 DNS服務
前面介紹了與HTTP協議有着密切關係的TCP/IP協議,接下來介紹的DNS服務也是與HTTP協議有着密不可分的關係。

通常我們訪問一個網站,使用的是主機名或者域名來進行訪問的。因為相對於IP地址(一組純数字),域名更容易讓人記住。但TCP/IP協議使用的是IP地址進行訪問的,所以必須有個機制或服務把域名轉換成IP地址。DNS服務就是用來解決這個問題的,它提供域名到IP地址之間的解析服務。

如下圖所示,展示了DNS服務把域名解析成IP地址的過程:

DNS服務是通過DNS協議進行通信的,而DNS協議跟HTTP協議一樣也是應用層協議。由於我們的重點是HTTP協議,所以這裏不打算對DNS協議進行詳細的分析,我們只需要知道可以通過DNS服務把域名解析成IP地址即可。

1.1.4 HTTP與TCP/IP、DNS的關係
到現在,我們介紹了與HTTP協議有密切關係的TCP/IP協議和DNS服務,接下來我們通過下圖來整理一下HTTP協議與它們之間的關係:

HTTP與TCP/IP、DNS的關係
從上圖中可以知道,當客戶端訪問Web站點時,首先會通過DNS服務查詢到域名的IP地址。然後瀏覽器生成HTTP請求,並通過TCP/IP協議發送給Web服務器。Web服務器接收到請求後會根據請求生成響應內容,並通過TCP/IP協議返回給客戶端。

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

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

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

drf組件之jwt認證

drf組件之jwt認證模塊

一、認證規則

全稱:json web token
解釋:加密字符串的原始數據是json,後台產生,通過web傳輸給前台存儲
格式:三段式 – 頭.載荷.簽名 – 頭和載荷用的是base64可逆加密,簽名用md5不可逆加密
內容:
頭(基礎信息,也可以為空):加密方式、公司信息、項目組信息、…
載荷(核心信息):用戶信息、過期時間、…
簽名(安全保障):頭加密結果+載荷加密結果+服務器秘鑰 的md5加密結果

認證規則:
後台一定要保障 服務器秘鑰 的安全性(它是jwt的唯一安全保障)
後台簽發token(login接口 ) -> 前台存儲 -> 發送需要認證的請求帶着token -> 後台校驗得到合法的用戶 -> 權限管理

為什麼要有jwt認證:
1)服務器壓力小, 後台不需要存儲token,只需要存儲簽發與校驗token的算法,效率遠遠大於後台存儲和取出token完成校驗
2) jwt算法認證,更適合服務器集群部署

二、認證模塊

安裝:pip install djangorestframework-jwt
模塊包:rest_framework_jwt

採用drf-jwt框架,後期任務只需要書寫登錄
為什麼要重寫登錄:drf-jwt只完成了賬號密碼登錄,我們還需要手機登錄,郵箱登錄
為什麼不需要重寫認證類:因為認證規則已經完成且固定不變,變得只有認證字符串的前綴,前綴可以在配置文件中配置

三、JWT使用

jwt配置;

在settings.py文件中配置,如果不配置,默認走jwt默認的

jwt插件的三個接口:

在urls.py中配置

在postman中測試一下籤發token

注意:上面三個接口都是發送POST請求

四、利用JWT實現多方式登錄

注:APIResponse 為自定義Response對象

# views.py
from rest_framework.views import APIView
from . import models,serializers
from utils.response import APIResponse

class LoginAPIView(APIView):
    # 登錄接口應該禁用所有的認證和、權限,因為不管是誰都應該能進來
    authentication_classes = []
    permission_classes = []
    def post(self, request, *args, **kwargs):
        # 將數據傳到序列化組件進行校驗
        user_ser = serializers.LoginSerializer(data=request.data)
        user_ser.is_valid(raise_exception=True)

        return APIResponse(msg='login success', data={
            'username': user_ser.user.username,
            'token': user_ser.token
        })

注意:

通過user對象生成payload載荷
payload = jwt_payload_handler(user)

通過payload簽發token
token = jwt_encode_handler(payload)

# serializer.py
from rest_framework.serializers import ModelSerializer, CharField, ValidationError, SerializerMethodField
from . import models
from django.contrib.auth import authenticate
import re
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler

class LoginSerializer(ModelSerializer):
    username = CharField(write_only=True)
    password = CharField(write_only=True)
    class Meta:
        model = models.User
        fields = ('username', 'password')

    def validate(self, attrs):
        # user_obj = authenticate(**attrs)
        # if not user_obj:
        #     raise ValidationError('用戶名或密碼錯誤')

        # 賬號密碼登錄 ==》 多方式登錄
        user = self._many_method_login(**attrs)

        # 通過user對象生成payload載荷
        payload = jwt_payload_handler(user)
        # 通過payload簽發token
        token = jwt_encode_handler(payload)

        # 將user和token存放在序列化對象中,方便返回到前端去
        self.user = user
        self.token = token

        return attrs

    # 多方式登錄 (用戶名、郵箱、手機號三種方式登錄)
    def _many_method_login(self, **attrs):
        username = attrs.get('username')
        password = attrs.get('password')
        # 利用正則匹配判斷用戶輸入的信息
        # 1.判斷郵箱登錄
        if re.match(r'.*@.*', username):
            user = models.User.objects.filter(email=username).first()  # type: models.User
        # 2.判斷手機號登錄
        elif re.match(r'^1[3-9][0-9]{9}$',username):
            user = models.User.objects.filter(mobile=username).first()
        # 3.用戶名登錄
        else:
            user = models.User.objects.filter(username=username).first()

        if not user:
            raise ValidationError({'username': '賬號有誤'})

        if not user.check_password(password):
            raise ValidationError({'password': '密碼錯誤'})

        return user

使用postman測試代碼:

五、前後台分離模式下信息交互規則

"""
1)任何人都能直接訪問的接口
    請求不是是get、還是post等,不需要做任何校驗

2)必須登錄后才能訪問的接口
    任何請求方式都可能做該方式的限制,請求必須在請求頭中攜帶認證信息 - authorization
    
3)前台的認證信息獲取只能通過登錄接口
    前台提供賬號密碼等信息,去後台換認證信息token
    
4)前台如何完成登錄註銷
    前台登錄成功一般在cookie中保存認證信息token,分離註銷就是前台主動清除保存的token信息
"""

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

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

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

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

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

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

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

快速搭建 SpringCloud 微服務開發環境的腳手架

本文適合有 SpringBoot 和 SpringCloud 基礎知識的人群,跟着本文可使用和快速搭建 SpringCloud 項目。

本文作者:HelloGitHub-秦人

HelloGitHub 推出的系列,今天給大家帶來一款基於 SpringCloud2.1 的微服務開發腳手開源項目——SpringCloud

項目源碼地址:

一、微服務的簡介

微服務是可以獨立部署、水平擴展、獨立訪問的服務單元。Java 中常見最小的微服務單元就是基於 SpringBoot 框架的一個獨立項目。一個微服務只做一件事(單一職責),多個微服務組合才能稱之為一個完整的項目或產品。那麼多個微服務的就需要來管理,而 SpringCloud 就是統籌這些微服務的大管家。它是一系列有序框架的集合,簡單易懂、易部署易維護的分佈式系統開發工具包。

今天介紹的開源項目就是基於 SpringCloud2.1 的腳手架,讓項目開發快速進入業務開發,而不需過多時間花費在架構搭建上,下面就讓我們一起來看看這個項目的使用吧。

二、項目結構

這裏以一個網關(gateway-admin)微服務來說明。

項目目錄結構如下圖:

目錄說明:

  1. db:項目初始化數據庫腳本。
  2. docker:Docker 配置文件目錄,將微服務打包為 docker 鏡像(image)。
  3. config:項目配置信息目錄,包括數據庫配置,消息轉化配置等。
  4. dao:數據庫操作目錄,主要對底層數據進行增刪查改。
  5. entity:項目實體類目錄。
  6. events:事件處理目錄。
  7. exception:異常處理目錄,通過面向切面處理全局異常。
  8. rest:微服務控制器目錄,也就是對外提供的接口。
  9. service:微服務業務層目錄。
  10. GatewayAdminApplication:微服務 SpringBoot 入口類。
  11. resources:項目配置文件目錄。
  12. test:項目單元測試目錄。
  13. pom.xml:maven 項目對象模型文件。

三、實戰操作

3.1 前提

  • 確保本地安裝 Git、Java8、Maven。
  • 懂一些 SpringMVC 的知識,因為 SpringBoot 是基於 SpringMVC 演化而來的。
  • 懂一些應用容器引擎 Docker、Docker-compose 的知識。

3.2 微服務架構說明

一個完整的項目,微服務架構一般包括下面這些服務:

  • 註冊中心(常用的框架 Nacos、Eureka)
  • 統一網關(常用的框架 Gateway、Zuul)
  • 認證中心(常用技術實現方案 Jwt、OAuth)
  • 分佈式事務(常用的框架 Txlcn、Seata)
  • 文件服務
  • 業務服務

3.3 運行項目

下面介紹了三種運行的方式:

第一種:一鍵運行

Linux 和 Mac 系統下可在項目根目錄下執行 ./install.sh 快速搭建開發環境。

第二種:本地環境運行

不推薦此方法,但還是簡單介紹下。

  1. 基礎環境安裝:mysql、redis,rabbitmq

  2. 環境運行:
    git clone https://github.com/zhoutaoo/SpringCloud.git #克隆項目

  3. 安裝認證公共包到本地 maven 倉庫,執行如下命令:
    cd common mvn clean install #安裝認證公共包到本地 maven 倉庫

  4. 安裝註冊中心 Nacos
    • 下載
    • 執行如下命令:

      unzip nacos-server-0.9.0.zip  OR tar -xvf nacos-server-0.9.0.tar.gz
      cd nacos/bin
      bash startup.sh -m standalone # Linux 啟動命令
      cmd startup.cmd # Windows 啟動命令
  5. 運行網關服務、認證服務、業務服務等

這裏以網關服務為例:執行 GatewayAdminApplication.java

注意:認證服務(auth)、網關服務(gateway)、組織管理服務(sysadmin)需要執行數據庫初始化腳本。

可通過 swager 接口: 測試是否搭建成功,如果能正常訪問表示服務啟動成功。

說明:

  • application.yml 文件主要配置 rabbitmq,redis, mysql 的連接信息。

    spring:
      rabbitmq:
        host: ${RABBIT_MQ_HOST:localhost}
        port: ${RABBIT_MQ_PORT:5672}
        username: ${RABBIT_MQ_USERNAME:guest}
        password: ${RABBIT_MQ_PASSWORD:guest}
      redis:
        host: ${REDIS_HOST:localhost}
        port: ${REDIS_PORT:6379}
        #password: ${REDIS_PASSWORD:}
        lettuce:
          pool:
            max-active: 300
    
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:${DATASOURCE_DBTYPE:mysql}://${DATASOURCE_HOST:localhost}:${DATASOURCE_PORT:3306}/sc_gateway?characterEncoding=UTF-8&useUnicode=true&useSSL=false
        username: ${DATASOURCE_USERNAME:root}
        password: ${DATASOURCE_PASSWORD:root123}
  • bootstrap.yml 文件主要配置服務基本信息(端口,服務名稱),註冊中心地址等。

    server:
      port: ${SERVER_PORT:8445}
    spring:
      application:
        name: gateway-admin
      cloud:
        nacos:
          discovery:
            server-addr: ${REGISTER_HOST:localhost}:${REGISTER_PORT:8848}
          config:
            server-addr: ${REGISTER_HOST:localhost}:${REGISTER_PORT:8848}
            file-extension: yml
        sentinel:
          transport:
            dashboard: ${SENTINEL_DASHBOARD_HOST:localhost}:${SENTINEL_DASHBOARD_PORT:8021}

第三種:Docker 環境運行

  1. 基礎環境安裝
    • 通過 docker 命令安裝

      # 安裝redis
      docker run -p 6379:6379 --name redis -d docker.io/redis:latest --requirepass "123456" 
      # 安裝mysql
      docker run --name mysql5.7 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root123 -d docker.io/mysql:5.7
      # 安裝rabbitmq 
      docker run -d -p 15672:15672 -p 5672:5672 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin --name rabbitmq docker.io/rabbitmq:latest
    • 也可以通過 docker-compose 命令安裝

      cd docker-compose
      docker-compose up -d  #docker-compose 安裝mysql,redis,rabbitmq 服務
  2. 下載項目到本地
    git clone https://github.com/zhoutaoo/SpringCloud.git #克隆項目

  3. 安裝認證公共包到本地 maven 倉庫執行如下命令:
    cd common && mvn install #安裝認證公共包到本地maven倉庫

  4. docker-compose 運行 Nacos
    cd docker-compose docker-compose -f docker-compose.yml -f docker-compose.nacos.yml up -d nacos #啟動註冊中心

  5. 構建消息中心鏡像
    cd ./center/bus mvn package && mvn docker:build cd docker-compose #啟動消息中心 docker-compose -f docker-compose.yml -f docker-compose.center.yml up -d bus-server

需要構建鏡像的其他服務有:(注:操作和消息中心鏡像構建方式類似)

  • 網關管理服務 (gateway-admin、gateway-web)

  • 組織服務(sysadmin/organization)

  • 認證服務 (auth/authentication-server)

  • 授權服務(auth authorization-server)

  • 管理台服務(monitor/admin)

3.4 運行效果

Nacos 服務中心

所有服務都正常啟動,在 nacos 管理中心可查看,實例數表示運行此服務的個數,值為 1 可以理解為服務正常啟動。

查看後台服務

命令行執行:docker ps -a 查看 docker 所有進程信息

通過訪問微服務對外暴露的接口(swagger)檢測服務是否可用。

swager 接口地址:

測試如下圖:

四、最後

微服務(SpringBoot、SpringCloud、Docker)現在吵得特別火,它並不是一門新的技術,而是在老技術的基礎上衍生出來的,增加了一些新的特性。

教程至此,你應該能夠通過 SpringCloud 這項目快速搭建微服務了。那麼就可以開始你的微服務學習之旅了,是時候更新一下自己的技能樹了,讓我們一起來學習微服務吧!

五、參考資料

『講解開源項目系列』——讓對開源項目感興趣的人不再畏懼、讓開源項目的發起者不再孤單。跟着我們的文章,你會發現編程的樂趣、使用和發現參与開源項目如此簡單。歡迎留言聯繫我們、加入我們,讓更多人愛上開源、貢獻開源~

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

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

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Web Scraper 翻頁——利用 Link 選擇器翻頁 | 簡易數據分析 14

這是簡易數據分析系列的第 14 篇文章。

今天我們還來聊聊 Web Scraper 翻頁的技巧。

這次的更新是受一位讀者啟發的,他當時想用 Web scraper 爬取一個分頁器分頁的網頁,卻發現我之前介紹的方法不管用。我研究了一下才發現我漏講了一種很常見的翻頁場景。

在 的文章里,我們講了如何利用 Element Click 選擇器模擬鼠標點擊分頁器進行翻頁,但是把同樣的方法放在 上,翻頁到第二頁時抓取窗口就會自動退出,一條數據都抓不到。

其實主要原因是我沒有講清楚這種方法的適用邊界。

通過 Element Click 點擊分頁器翻頁,只適用於網頁沒有刷新的情況,我在那篇文章里舉了蔡徐坤微博評論的例子,翻頁時網頁是沒有刷新的:

仔細看下圖,鏈接發生了變化,但是刷新按鈕並沒有變化,說明網頁並沒有刷新,只是內容變了

而在 豆瓣 TOP 250 的網頁里,每次翻頁都會重新加載網頁:

仔細看下圖,鏈接發生變化的同時網頁刷新了,有很明顯的 loading 轉圈動畫

其實這個原理從技術規範上很好解釋:當一個 URL 鏈接是 # 字符后數據變化時,網頁不會刷新;當鏈接其他部分變化時,網頁會刷新。當然這個只是隨口提一下,感興趣的同學可以去研究一下,不感興趣可以直接跳過。

1.創建 Sitemap

本篇文章就來講解一下,如何利用 Web Scraper 抓取翻頁時會刷新網頁的分頁器網站。

這次的網頁我們選用練手 Web Scraper 的網站——,換個姿勢練習 Web Scraper 翻頁技巧。

像這種類型的網站,我們要藉助 Link 選擇器來輔助我們翻頁。Link 標籤我們在介紹過了,我們可以利用這個標籤跳轉網頁,抓取另一個網頁的數據。這裏我們利用 Link 標籤跳轉到分頁網站的下一頁

首先我們用 Link 選擇器選擇下一頁按鈕,具體的配置可以見下圖:

這裡有一個比較特殊的地方:Parent Selectors ——父選擇器。

之前我們都沒有碰過這個選擇框的內容,**next_page 這次要有兩個父節點——_root 和 next_page**,鍵盤按 shift 再鼠標點選就可以多選了,先按我說的做,後面我會解釋這樣做的理由。

保存 next_page 選擇器后,在它的同級下再創建 container 節點,用來抓取電影數據:

這裏要注意:翻頁選擇器節點 next_page 和數據選擇器節點 container 是同一級,兩個節點的父節點都是兩個:_root 和 next_page:

因為重點是 web scraper 翻頁技巧,抓取的數據上我只簡單的抓取標題和排名:

然後我們點擊 Selector graph 查看我們編寫的爬蟲結構:

可以很清晰的看到這個爬蟲的結構,可以無限的嵌套下去:

點擊 Scrape,爬取一下試試,你會發現所有的數據都爬取下來了:

2.分析原理

按照上面的流程下來,你可能還會比較困擾,數據是抓下來了,但是為什麼這樣操作就可以呢,**為什麼 next_page 和 container 要同級,為什麼他們要同時選擇兩個父節點:_root 和 next_page?**

產生困擾的原因是因為我們是倒敘的講法,從結果倒推步驟;下面我們從正向的思維分步講解。

首先我們要知道,我們抓取的數據是一個樹狀結構,_root 表示根節點,就是我們的抓取的第一個網頁,我們在這個網頁要選擇什麼東西呢?

1.一個是下一頁的節點,在這個例子里就是用 Link 選擇器選擇的 next_page

2.一個是數據節點,在這個例子里就是用 Element 選擇器選擇的 container

因為 next_page 節點是會跳轉的,會跳到第二頁。第二頁除了數據不一樣,結構和第一頁還是一樣的,為了持續跳轉,我們還要選擇下一頁,為了抓取數據,還得選擇數據節點:

如果我們把箭頭反轉一下,就會發現真相就在眼前,next_page 的父節點,不正好就是 _root 和 next_page  嗎?container 的父節點,也是 _root 和 next_page!

到這裏基本就真相大白了,不理解的同學可以再多看幾遍。像 next_page 這種我調用我自己的形式,在編程里有個術語——遞歸,在計算機領域里也算一種比較抽象的概念,感興趣的同學可以自行搜索了解一下。

3.sitemap 分享

下面是這次實戰的 Sitemap,同學們可以導入到自己的 web scraper 中進行研究:

{"_id":"douban_movie_top_250","startUrl":["https://movie.douban.com/top250?start=0&filter="],"selectors":[{"id":"next_page","type":"SelectorLink","parentSelectors":["_root","next_page"],"selector":".next a","multiple":true,"delay":0},{"id":"container","type":"SelectorElement","parentSelectors":["_root","next_page"],"selector":".grid_view li","multiple":true,"delay":0}]}

4.推薦閱讀

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

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

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

以太網驅動的流程淺析(一)-Ifconfig主要流程【原創】

以太網驅動的流程淺析(一)-Ifconfig主要流程

Author:張昺華
Email:920052390@qq.com
Time:2019年3月23日星期六

此文也在我的個人公眾號以及《Linux內核之旅》上有發表:

很喜歡一群人在研究技術,一起做有意思的東西,一起分享技術帶給我們的快樂,也希望中國有更多的人熱愛技術,喜歡一起研究、分享技術,然後可以一起用我們的技術來做一些好玩的東西,可以為這個社會創造一些東西來改善人們的生活。
如下是本人調試過程中的一點經驗分享,以太網驅動架構畢竟涉及的東西太多,如下僅僅是針對加載流程和圍繞這個問題產生的分析過程和驅動加載流程部分,並不涉及以太網協議層的數據流程分析。

【硬件環境】 Imx6ul

【Linux kernel版本】 Linux4.1.15

【以太網phy】 Realtek8201f

一個以太網的案例來講述Ifconfig

1. 問題描述

【問題】

機器通過usb方式下載了mac地址后,發現以太網無法正常使用,敲命令 ifconfig eth0 up出現:ifconfig: SIOCSIFFLAGS: No such device,而對於沒有下載以太網mac address的機器表現均正常。調試過程中發現在以太網控制器代碼中加入一些printk,不正常的機器又正常了,打印的位置不同,機器的以太網有時會正常,有時會異常,十分詭異。

2. 原因分析

【根本原因】

reset時序問題導致,phy reset的時間不滿足時序要求。如下圖,如果硬件接了reset引腳,應滿足時序要求在reset保持10ms有效電平后,還必須維持至少150ms才可以訪問phy register,也就是reset要在B點之後才可以正常通過MDC/MDIO來訪問phy register。如果是不使用硬件reset,使用軟件reset方式,那也要至少在A點,也就是在reset維持10ms有效電平后,再維持3.5個clk才能正常訪問phy register。

那為什麼下載了mac地址后才異常呢?不下載的又正常呢?

【原因分析】

freescale控制器獲取mac address流程如下:
1)模塊化參數設置,如果沒有跳到步驟2;
2)device tree中設置,如果沒有跳到步驟3;
3)from flash / fuse / via platform data,如果沒有跳到步驟4;
4)FEC mac registers set by bootloader===》即靠usb方式下載mac address ,如果沒有跳到步驟5;
5)靠kernel算一個隨機數mac address出來,然後寫入mac

那為什麼下載了mac地址后才異常呢?
下了mac后,會執行步驟4,不會執行步驟5,此時目前的代碼不滿足150ms的時序要求,無法訪問phy register,
導致phy_id獲取不到,因此phy_device也不會創建

那為什麼不下載的又正常呢?
不下載mac address,會執行步驟5 ,步驟5中調用了函數eth_hw_addr_random
剛好滿足了150ms的時序要求,所以才可以正常

跟入代碼eth_hw_addr_random看下

繼續看:

最終調用了kernel提供的獲取隨機數的一個函數,這塊代碼比較多就不繼續追下去了。

所以這塊步驟五的代碼剛剛好好在這個硬件條件下,恰巧滿足了150ms的reset時序要求,所以以太網才可以正常。

3. 以太網流程分析跟蹤

3.1 Ifconfig主要流程

回歸主題,根據這個ifconfig失敗的現象,我們追蹤一下code:
ifconfig: SIOCSIFFLAGS: No such device,既然出現了這個問題log,我們就從應用層的log入手,首先我們使用strace命令來追蹤下系統調用,以便於我們追蹤內核代碼實現。
strace ifconfig eth0 up跟蹤一下

可以發現主要是ioctl的操作,SIOCSIFFLAGS,然後我們需要了解下這個宏的意思,說白了就是設置各種flag,靠ioctl第三個參數把所需要的動作flag傳入,比如說此時要對eth0進行up動作,那麼就傳入IFF_UP,例如:
struct ifreq ifr;

我們看這些主要是想知道為什麼會打印這個log:
ifconfig: SIOCSIFFLAGS: No such device
那麼內核中又是對ioctl做了什麼動作呢?因為strace命令讓我們知道了系統調用調用函數,我們可以在kernel中直接搜索SIOCSIFFLAGS,或者去以太網驅動net目錄下直接搜索更快。最終我搜到了,路徑是:net/ipv4/devinet.c
我們可以看到內核的宏定義:

查看devinet.c的代碼,我們找到了那個宏,也就是做devinet_ioctl函數中,這也就是應用層的ioctl最終的實現函數,然後我們在裏面加一些打印,

通過打印結果我們可以確認是這個函數devinet_ioctl為應用層的ioctl的實現函數,因為你在kernel中搜SIOCSIFFLAGS宏的話會有很多地方出現的,所以我們需要確認我們找的函數
沒問題:

看到這裏返回值ret是-19,那麼我們繼續順着追蹤下去,上代碼:
net/core/dev.c

繼續追蹤:net/core/dev.c

因此我們可以看到返回值-19就是如下代碼產生的

因此我們需要追蹤__dev_open函數,繼續看代碼:

通過調試,比如說加打印,或者是經驗我們可以推斷出是這裏返回的-19,那麼這個ndo_open又是在哪裡回調的呢?

我們可以看到ops這個結構的結構體
struct net_device dev
const struct net_device_ops
ops = dev->netdev_ops;

這裏熟悉驅動的朋友應該可以猜到這在在freescale的以太網控制器驅動中一定有它的實現
net_device_ops就是kernel提供給drvier操作net_device的一些操作方法,具體實現自然由相應廠商的driver自己去實現。
路徑:drivers/net/Ethernet/freescale/fec_main.c

我們可以在這個fec_enet_open函數中加入dump_stack來看下整個調用情況
我們打出kernel的dump_stack信息來看:

這個調用過程就是應用層ioctl一直到kernel最底層fec_enet_open的過程。
應用代碼這樣:

總體流程:kill() -> kill.S -> swi陷入內核態 -> 從sys_call_table查看到sys_kill -> ret_fast_syscall -> 回到用戶態執行kill()下一行代碼
Ioctl《==ret_fast_syscall 《==SyS_ioctl《==do_vfs_ioctl《==vfs_ioctl《==sock_ioctl《==
devinet_ioctl《==dev_change_flags《==__dev_change_flags《==__dev_open《==fec_enet_open
我附上每個函數的代碼:
如果大家想看系統調用流程的話,參考這篇,我就不做這塊的說明了:
Linux系統調用(syscall)原理
http://gityuan.com/2016/05/21/syscall/
Arm Linux系統調用流程詳細解析
https://www.cnblogs.com/cslunatic/p/3655970.html

4. 網址分享

http://stackoverflow.com/questions/5308090/set-ip-address-using-siocsifaddr-ioctl
http://www.ibm.com/support/knowledgecenter/ssw_aix_72/com.ibm.aix.commtrf2/ioctl_socket_control_operations.htm
https://lkml.org/lkml/2017/2/3/396

http://www.latelee.org/programming-under-linux/linux-phy-driver.html
Linux PHY幾個狀態的跟蹤
http://www.latelee.org/programming-under-linux/linux-phy-state.html
第十六章PHY -基於Linux3.10
https://blog.csdn.net/shichaog/article/details/44682931

“`

End

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

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

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!