高屋建瓴:梳理編程約定


相關文章連接:

編程之基礎:數據類型(一)

編程之基礎:數據類型(二)

動力之源:代碼中的“泵”

完整目錄與前言

高屋建瓴:梳理編程約定

在實際編程中,我們會遇見各種各樣的概念,雖然有的並沒有官方定義,但是我們可以自己給它取一個形象的名稱。本章總結了13條會在本書中出現的概念。

2.1 代碼中的Client與Server

我們一般說到Client和Server,就會聯想到網絡通訊,TCP、UDP或者Socket這些概念馬上就浮現在頭腦中。其實Client和Server不僅僅可以用來形容網絡通訊雙方,它還可以用來形容代碼中兩個有交互的代碼塊。

通訊結構中的Client與Server有信息交互,一般Server為Client提供服務。代碼中的一個方法調用另外一個對象的方法,同樣涉及到信息交互,也同樣可以看作這個對象為其提供服務。見下圖2-1:

圖2-1 Client與Server關系圖

圖2-1中"對象"稱作Server,Client與Server可以不在一個程序集中,也可以不在同一個AppDomain里,更可以不在一個進程中,甚至都不在一台主機上。下面代碼演示了Client與Server的關系:

 1 //Code 2-1
 2 
 3 class A
 4 {
 5     //
 6     public int DoSomething(int a,int b)
 7     {
 8         //do something here
 9         return a+b;
10     }
11 }
12 class Program
13 {
14     static void Main()
15     {
16         A a = new A();
17         int result = a.DoSomething(3,4); //invoke public method a.DoSomething
18     }
19 }

代碼Code 2-1中a對象是Server,Program是Client,前者為后者提供服務。

注:ClientServer不一定指的是對象,A程序集調用B程序集中的類型,我們可以把A當作Client,把B當作ServerClientServer也不是絕對的,在一定場合,Client也可以看作是Server

2.2 方法與線程的關系

線程和方法沒有一對一的關系,也就是說,一個線程可以調用多個方法,而一個方法又可以被多個線程調用。由於在代碼中,我們看得見的只有方法,因此有時候我們很難分清楚某個方法到底會運行在哪個線程之中。

 1 //Code 2-2
 2 
 3 class Program
 4 {
 5     static void DoSomething()
 6     {
 7         //do something here
 8     }
 9     static void Main()
10     {
11         //
12         Thread th1 = new Thread(new ThreadStart(DoSomething));
13         Thread th2 = new Thread(new ThreadStart(DoSomething));
14         th1.Start();
15         th2.Start();
16     }
17 }

代碼Code 2-2中的DoSomething方法可以同時運行在兩個線程當中。以上代碼還是比較直觀的情況,有時候,一點線程的影子都看不見,

 1 //Code 2-3
 2 
 3 class Form1:Form
 4 {
 5     //
 6     private void DoSomething()
 7     {
 8         //do something here
 9         //maybe invoke UI controls
10     }
11     private btn1_Click(object sender,EventArgs e)
12     {
13         BackgroundWorker back = new BackgroundWorker();
14         back.DoWork += back_DoWork;
15         back.Start();
16         DoSomething(); //NO.1
17     }
18     private void back_DoWork(object sender,DoWorkEventArgs e)
19     {
20         DoSomething(); //NO.2
21     }
22 }

上面代碼Code 2-3中有兩處調用了DoSomething方法,一個在btn1.Click的事件處理程序中,一個在back.DoWork的事件處理程序中,前者在UI線程中運行,而后者在非UI線程中運行,兩者可以同時進行。

當我們不確定我們編寫的方法到底會在哪些線程中運行時,我們最好需要特別注意一下,如果方法訪問了公共資源,多個線程同時執行這個方法時可能會引起資源異常。另外,只要我們確定了兩個方法只會運行在同一個線程中,那么這兩個方法不可能同時執行,跟方法處在的位置無關,

 1 //Code 2-4
 2 
 3 class Form1:Form
 4 {
 5     //
 6     private void DoSomething()
 7     {
 8         //do something here
 9         //maybe invoke UI controls
10     }
11     private void btn1_Click(object sender,EventArgs e)
12     {
13         DoSomething(); //NO.1
14     }
15     private void btn2_Click(object sender,EventArgs e)
16     {
17         DoSomething(); //NO.2
18     }
19 }

上面代碼Code 2-4中btn1.Click和btn2.Click的事件處理程序中都調用了DoSomething方法,但是由於btn1.Click和btn2.Click的事件處理程序都在UI線程中運行,所以這兩處的DoSomething方法不可能同時執行,只可能一前一后,此時我們不需要考慮方法中訪問的公共資源是否線程安全。

注:正常情況下,上面的結論成立,但是如果你非要在DoSomething中寫了一些特殊代碼,比如Application.DoEvents(),那么情況就不一定了,很有可能在btn1_Click中的DoSomething方法中調用btn2_Click方法,從而造成DoSomething方法還未結束,另一個DoSomething方法又開始執行,這個涉及到Windows消息循環的知識,本書第八章有講到。

2.3 調用線程與當前線程

前一節中說明了線程與方法的關系,一個線程很少只調用一個啟動方法,多數情況下,啟動方法中會調用其它方法,一個方法在哪個線程中運行,那么這個線程就是它的當前線程,

 1 //Code 2-5
 2 
 3 class A
 4 {
 5     public void DoSomething()
 6     {
 7         //do something here
 8         Console.WriteLine("currentthread is " + Thread.CurrentThread.Name);
 9     }
10 }
11 class Program
12 {
13     //the start method of main thread
14     static void Main()
15     {
16         A a = new A();
17         a.DoSomething(); //NO.1
18         Thread th1 = new Thread(new ThreadStart(th1_proc));
19         th1.Start();
20     }
21     static void th1_proc()
22     {
23         A a = new A();
24         a.DoSomething(); //NO.2
25     }
26 }

上面代碼Code 2-5中,在NO.1處,主線程就是調用線程,它調用了a.DoSomething方法,這時候a.DoSomething中會輸出主線程的Name屬性值。在NO.2處,th1才是調用線程,它調用了a.DoSomething方法,這時候a.DoSomething中會輸出th1線程的Name屬性值。

也就是說,哪個線程調用了方法,哪個線程就叫做這個方法的調用線程,方法在哪個線程中運行,哪個線程就是該方法的當前線程。

2.4 阻塞方法與非阻塞方法

首先,阻塞和非阻塞的概念是相對的,一個方法耗時很長才能返回,返回之前會一直阻塞調用線程,我們叫它阻塞方法;相反,一個方法耗時短,一調用馬上就返回,我們叫它非阻塞方法。但是這個"很長"與"很短"根本就沒有標准,

 1 //Code 2-6
 2 
 3 class Program
 4 {
 5     static void Func1()
 6     {
 7         for(int i=0;i<100;++i)
 8         {
 9             Thread.Sleep(10);
10         }
11     }
12 
13     static void Func2()
14     {
15         for(int i=0;i<100;++i)
16             for(int j=0;j<100;++j)
17             {
18                 Thread.Sleep(10);
19             }
20     }
21     static void Main()
22     {
23         Func1(); //NO.1
24         Console.WriteLine("Func1 over");
25         Func2(); //NO.2
26         Console.WriteLine("Func2 over");
27     }
28 }

上面代碼Code 2-6中,Func1相對於Func2來講,耗時短,我們把Func1叫做非阻塞方法,Func1不會阻塞它的調用線程,下面的Console.WriteLine很快就會執行;而相反,Func2耗時長,我們把Func2叫做阻塞方法,Func2會阻塞它的調用線程,下面的Console.WriteLine不能馬上執行。

現實編程中,也沒有嚴格標准,如果一個方法有可能耗時長,那么就把它當作阻塞方法。在編程中,需要注意阻塞方法和非阻塞方法的使用場合,有的線程中不應該調用阻塞方法,比如Winform中的UI線程。

有時候一個類會提供兩個功能相同的方法,一種是阻塞方法,它會阻塞調用線程,一直等到任務執行完畢才返回,另一種是非阻塞方法,不管任務有沒有執行完畢,馬上就會返回,不會阻塞調用線程,至於任務何時執行完畢,它會以另一種方式通知調用線程。這兩種調用方式也稱為"同步調用"和"異步調用",FileStream.Read和FileStream.BeginRead就屬於這一類。

注:同步調用和異步調用在后面的章節會講到,異步編程模型(Asynchronous Programming Model)是.NET中一項重要技術。我們既可以把耗時10S的方法稱為阻塞方法,也可以把耗時100MS的方法稱為阻塞方法,理論上沒有標准。

2.5 UI線程與線程

UI線程一般出現在Winform編程中,主要負責用戶界面(User Interface)的消息處理。本質上,UI線程跟普通線程沒有什么區別。

一個線程只有不停地循環去處理任務才不會馬上終止,也就是說,線程必須想辦法去維持它的運行,不然很快就會運行結束。UI線程中包含一個Windows消息循環,使用常見的While結構實現,該循環不停地獲取用戶輸入,包括鼠標、鍵盤等輸入信息,然后不停地處理這些信息,正因為有這樣一個While循環存在,UI線程才不會一開始就馬上結束。

圖2-2 一個維持線程運行的循環結構

Winform中的UI線程默認由Program.Main方法中的Application.Run()進入,While循環結構存在於Application.Run()內部(或者由其調用)。

注:詳細的Windows消息循環,請參見第八章。使用任何語言開發的Windows桌面應用程序都至少包含一個UI線程。

2.6 原子操作

所謂"原子",即不可再分的意思。代碼中的原子操作指代碼執行的最小單元,也就是說,原子操作不可以被中斷,它只有三個狀態:未執行、正在執行和執行完畢,絕對沒有執行到一半暫停下來,等待一會,又繼續執行的情況。原子操作又稱為程序中不可以被線程調度打斷的操作。

比如給一個整型變量賦值"int a = 1;",這個操作就是原子操作,不可能有給a賦值到一半,操作暫停的情況。相反有很多操作,屬於非原子操作,比如對一些集合容器的操作,向某些集合容器中增加刪除元素等操作都是非原子操作,這些操作可能被打斷,出現操作一半暫停的情況。非原子操作由許許多多的原子操作組成。

圖2-3 原子操作與非原子操作

圖2-3中虛線框表示一個非原子操作,一個非原子操作由多個原子操作組成,虛線框中的操作可能在NO.1、NO.2、 NO.3任何一個地方暫停。

注:原子操作與非原子操作不是以代碼行數來區分的,是以這個操作在底層怎么實施去區分的,比如"a++;"只有一行代碼,但是它不是原子操作,它底層實現是由許多原子操作組合而成。

2.7 線程安全

我們操作一個對象(比如調用它的方法或者給屬性賦值),如果該操作為非原子操作,也就是說,可能操作還沒完成就暫停了,這個時候如果有另外一個線程開始運行同時也操作這個對象,訪問了同樣的方法(或屬性),這個時候可能會出現一種問題:前一個操作還未結束,后一個操作就開始了,前后兩個操作一起就會出現混亂。

當多個線程同時訪問一個對象(資源)時,如果每次執行可能得到不一樣的結果,甚至出現異常,我們認為這個對象(資源)是"非線程安全"的。造成一個對象非線程安全的因素有很多,上面提到的由於非原子操作執行到一半就中斷是一種,還有一種情況是多CPU情況中,就算操作沒有中斷,由於多個CPU可以真正實現多線程同時運行,所以還是有可能出現"對同一對象同時操作出現混亂"的情況。

圖2-4 兩種可能引起非線程安全的情況

圖2-4中左邊兩個線程運行在單CPU系統中,A線程中的非原子操作中斷,對R的操作暫停,B線程開始操作R,前后兩次操作相互干擾,可能出現異常。圖中右邊兩個線程運行在雙CPU中,無論操作是否中斷,都可能出現兩個操作相互干擾的情況。

為了解決多線程訪問同一資源有可能引起的不穩定性,我們需要在操作方法中做一些改進,最常見的是:對可能引起不穩定的操作加鎖。在代碼中使用lock代碼塊、互斥對象等來實現。如果一個對象,在多個線程訪問它時,不會出現結果不穩定或異常情況,我們稱該對象為"線程安全"的,也稱訪問它的方法是"線程安全"的。

 1 //Code 2-7
 2 
 3 class A
 4 {
 5     //
 6     int _a1 = 0;
 7     int _a2 = 0;
 8     object _syncObj = new object();
 9     public Int Result
10     {
11         get
12         {
13             lock(_syncObj)
14             {
15                 if(_a2!=0) //NO.1
16                 {
17                     return _a1/_a2; //NO.2
18                 }
19                 else
20                 {
21                     return 0;
22                 }
23             }
24         }
25     }
26     public void DoSomething(int a1, int a2)
27     {
28         lock(_syncObj)
29         {
30             _a1 = a1;
31             _a2 = a2;
32         }
33     }
34     //other public methods
35 }

上面代碼Code 2-7中,單CPU時,如果沒有lock塊,多線程訪問A類對象,一個線程在訪問A.Result屬性時,在判斷if(_a2!=0)為true后,可能在NO.1之后和NO.2之前處出現中斷(線程掛起),此時另一線程通過DoSomething方法修改_a2的值為0,中斷恢復后,程序報錯。雙CPU中,如果沒有lock塊,多線程訪問A類對象,情況更糟,一個線程訪問A.Result屬性時,不管在NO.1之后和NO.2之前會不會中斷,另一個線程都有可能通過DoSomething方法修改_a2的值為0,程序報錯。

另外,在Winform編程中,我們常遇見的"不在創建控件的線程中訪問該控件"的異常,原因就是對UI控件的操作幾乎都不是線程安全的(部分是),一般UI控件只能由UI線程操作,其余的所有操作均需要投遞到UI線程之中執行,否則就像前面講的,程序出現異常或不穩定。

 1 //Code 2-8
 2 
 3 class Form1:Form
 4 {
 5     //
 6     private btn1_Click(object sender,EventArgs e)
 7     {
 8         DealControl(null); //NO.1
 9         Thread th1 = new Thread(new ThreadStart(th1_proc));
10         th1.Start();
11     }
12     private void th1_proc()
13     {
14         DealControl(null); //NO.2
15     }
16     private void DealControl(object[] args)
17     {
18         //
19         if(this.InvokeRequired) //NO.3
20         {
21             this.Invoke((Action)delegate() //NO.4
22             {
23                 DealControl(args);
24             });
25         }
26         else
27         {
28             //access ui controls directly
29             //
30         }
31     }
32 }

上面代碼Code 2-8中,DealControl方法中需要操作UI控件,如果我們不知道DealControl到底會在哪個線程中運行,有可能在UI線程也有可能在非UI線程,那么我們可以使用Control.InvokeRequired屬性去判斷當前線程是否是創建控件的線程(UI線程),如果是,則該屬性返回false,可以直接操作UI控件,否則,返回true,不能直接操作UI控件。代碼中NO.1處直接在UI線程中調用DealControl,DealControl中可以直接操作UI控件,NO.2處在非UI線程中調用DealControl,那么此時,就需要將所有的操作通過Control.Invoke投遞封送到UI線程之中執行。

注:Control包含有若干個線程安全的方法和屬性,我們可以在非UI線程中使用它們。有Control.InvokeRequired屬性、Control.InvokeControl.BeginInvokeControl.Invoke的異步版本,后續章節有講到)、Control.EndInvoke以及Control.CreateGraphics方法。跨線程訪問這些方法和屬性不會引起異常。

2.8 調用(Call)與回調(CallBack)

調用(Call)和回調(CallBack)是編程中最常遇見的概念之一,幾乎出現在代碼中的每一處,只是許多人並沒有在意。現在最流行的解釋是:調用指我們調用系統的方法,回調指系統調用我們寫的方法。類似下面圖2-5描述的:

圖2-5 調用與回調的區別

上圖2-5是目前對"調用"和"回調"的解釋。但是需要清楚一點,本章第一節中已經講到過,客戶端(圖中Client)並不是絕對的,也就是說,Client也有可能成為圖中的"系統"部分,別人再調用它,它再回調另一個client。

圖2-6 程序中調用與回調的關系

圖2-6中描述一個程序中調用與回調的關系,我們平常對"調用"和"回調"的定義只局限在圖中虛線框中,它只是一個小范圍的規定。嚴格意義上講,不應該有調用和回調之分,因為所有代碼最終均由操作系統調用(甚至更底層)。

.NET中的回調主要是通過委托(Delegate)來實現的,委托是一種代理,專門負責調用方法(委托的詳細信息在本書第五章有講到)。

2.9 托管資源與非托管資源

其實這里的"托管"跟第一章中講到的托管環境、托管代碼或者托管時代中的"托管"意思一樣。在.NET中,對象使用的資源分兩種:一種是托管資源,一種是非托管資源。托管資源由CLR管理,也就是說不需要開發人員去人工控制,相對開發人員來講,托管資源的管理幾乎可以忽略,.NET中托管資源主要指"對象在堆中的內存"等;非托管資源指對象使用到的一些托管環境以外(比如操作系統)的資源,CLR不會管理這些資源,需要開發人員人工去控制。.NET中對象使用到的非托管資源主要有I/O流、數據庫連接、Socket連接、窗口句柄等各種直接與操作系統相關的資源。

圖2-7 一個堆中對象使用的資源

圖2-7中虛線框表示"可能有",即一個堆中對象可能使用到了非托管資源,但是它一定使用了托管資源。一個對象在使用完畢后(進入不可達狀態,並不是死亡,第四章會講到區別),我們應該確保它使用的(如果使用了)非托管資源能夠及時釋放,歸還給操作系統,至於托管資源,我們大部分時間不需要去關心,因為CLR(具體應該是Garbage Collector)會幫我們處理。.NET中使用了非托管資源的類型有很多,比如FileStream、Socket、Font、Control(及其派生類)、SqlDataConnection等等,它們內部封裝了非托管資源,沒有使用非托管資源的類型也有很多,比如Console、EventArgs、ArrayList等等。

怎么完美地處理一個對象使用的非托管資源,是一門相當重要而且必學的技術,后面第四章有詳細提到。

注:現在普遍有一種錯誤的觀點就是,將FileStreamSocket這樣的類型對象稱為非托管資源,這個是錯誤的,只能說這些對象使用到了非托管資源。

2.10 框架(Frameworks)與庫(Library)

框架和類庫都是一系列可以被重用的代碼集合。不同的是,框架算是不完整的應用程序,理論上,我們不用寫任何代碼,框架本身可以運行起來;而類庫多半指能夠提供一些具體功能的類集合,它包含的內容和功能一般比框架更簡單。我們使用框架去開發一個應用程序,其實就是在框架的基礎上寫一些擴展代碼,框架就像一個沒有裝修的毛坯房屋,我們需要給它各種裝飾,在這個過程中,我們可以使用類庫,因為類庫可以為我們提供一些封裝好了的功能。下圖2-8為框架、程序(開發人員編寫)以及類庫三者之間的關系:

圖2-8 框架程序類庫之間的關系

圖2-8中的調用關系其實是雙向的,畫出的箭頭只顯示了主要調用關系,即框架調用開發人員代碼,后者再選擇性調用一些類庫。

從上圖2-8中我們可以看出,整個應用程序的最終控制權並不在開發人員手中,而是在框架方,這種現象稱為"控制轉換"(Inversion Of Control,IOC),即程序的運行流程由框架控制,幾乎所有框架都遵循這個規則。

 1 //Code 2-9
 2 
 3 class Program
 4 {
 5     //
 6     static int GetTotal(int first,int second)
 7     {
 8         return first + second;
 9     }
10     static void Main()
11     {
12         int first,second;
13         Console.WriteLine("Input first:");
14         first = int.Parse(Console.ReadLine()); //NO.1
15         Console.WriteLine("Input second:");
16         second = int.Parse(Console.ReadLine()); //NO.2
17         int total = GetTotal(first,second); //NO.3
18         Console.WriteLine("the total is:" + total);
19         Console.Read();
20     }
21 }

上面代碼Code 2-9演示了從控制台程序(不使用框架開發)中獲取用戶輸入的兩個數據,然后輸出兩個數據之和,每個步驟的方法均由我們自己調用(NO.1. NO.2以及NO.3)。如果我們采用Winform程序(使用框架開發)實現,代碼如下:

 1 //Code 2-10
 2 
 3 class Form1:Form
 4 {
 5     public Form1()
 6     {
 7         //
 8         this.btn1.Click+=(EventHandler)(delegate(object sender,EventArgs e)
 9         {
10             int first = int.Parse(txtFirst.Text); //NO.1
11             int second = int.Parse(txtSecond.Text); //NO.2
12             int total = GetTotal(first,second); //NO.3
13             MessageBox.Show("the total is:" + total);
14         });
15     }
16     private int GetTotal(int first,int second)
17     {
18         return first + second;
19     }
20 
21 }

上面代碼Code 2-10演示了從窗體界面中的txtFirst和txtSecond兩個文本框中獲取數據,然后計算出兩個數據之和,每個步驟的方法都是由系統(框架)調用(在btn1.Click事件處理程序中)。使用框架開發的程序,代碼中大部分方法都屬於"回調方法"。

注:"控制轉換原則"又稱為"Hollywood Principle",即Don't call us, we will call you.意思是指好萊塢制片公司會主動聯系演員,而不需要演員自己去找電影制片公司。

2.11 面向(或基於)對象與面向(或基於)組件

這四個概念中最為熟悉的當然是"面向對象",其它三個離我們有點遙遠,平時接觸不多。

基於對象:如果一種編程語言有封裝的概念,能夠將數據和操作封裝在一起,形成一個整體,同時它又不具備像繼承、多態這些OO特性,那么就說這種語言是基於對象的,比如JavaScript。

面向對象:在基於對象的基礎之上,還具備繼承、多態特性的編程語言,我們稱該編程語言是面向對象的,比如C#,Java。

基於組件:組件是共享二進制代碼的基本單元,它是一個已經編譯完成的模塊,可以在多個系統中重用。在軟件開發中,我們事先定義好固定接口,然后將各個功能分開獨立開發,最后生成各自獨立的模塊。程序運行之后,分別加載這些獨立的模塊,各個模塊負責完成自己的功能,我們稱這種開發模式是基於組件的。基於組件開發模式中,除了二進制代碼可以重用外,還有另外一個優點,如果我們需要更新某一功能,或修復某一功能中的bug,在不改變原有接口前提下,我們不用重新編譯整個程序的源代碼,而只需要重新編譯某個組件源碼即可。組件應該是語言獨立的,一種語言開發出來的組件,理論上任何一種語言都可以使用它。

面向組件:基於組件開發中,我們只能重用已經編譯完成的二進制代碼,並不能從這個已經編譯好的組件中讀取其它信息,比如識別組件中的類型信息,派生出新的類型。面向組件指,在開發過程中,我們不僅能夠重用組件中的代碼,還能以該組件為基礎,擴展出新的組件,比如我們可以識別.NET程序集中的類型信息,以此派生出新的類型。.NET開發便是一種面向組件的開發模式。

注:如果說面向對象是強調類型與類型之間的關系,那么面向組件就是強調組件與組件之間的關系。另外,我們需要知道,.NET中的組件(程序集)並不包含傳統意義的二進制代碼。

2.12 接口

我們在閱讀一些書籍或者網上瀏覽一些文章時,經常會碰到"接口"的概念,比如"一個類應該盡可能少的對外提供公共接口"、"我們應該先取得淘寶的支付接口權限"、"繪制圖形時,我們需要調用系統的DrawImage接口"等等。那么,接口到底是什么?

其實我們碰到的這些"接口"概念跟它字面意思一樣:對外提供的、可以完成某項具體功能的通道。比如我們電腦上的USB口,通過它,我們能夠與電腦傳輸數據,還比如電視機的音量按鈕,通過它,我們可以調節電視機喇叭發出聲音的大小。接口是外界與系統(或模塊)內部通訊的通道。

    注:"接口"的概念基於"封裝"前提之上,如果沒有"封裝",那么就沒有"外界"與"內部"之說。

在軟件一般架構設計圖中,接口用以下表示:

圖2-9 接口示意圖

如上圖2-9所示,圓圈代表對外公開的通道,S的內部細節對外界C是不可見的。注意圖中的S不一定代表一個類,它可以是一個系統(跟C所屬不同的系統)、一個模塊或者其它具有"封裝"效果的單元個體。下圖2-10顯示某些場合存在的接口:

圖2-10 各種場合下的接口

如上圖2-10顯示了各種場合中的接口,可以看到,接口的概念不僅局限在代碼層面。下表2-1顯示了各種接口的表現形式:

表2-1各種場合中接口的具體表現形式

序號

場合

接口的表現形式

誰是外界

說明

1

類的公開方法,如

People p = new People();

p.Walk();

類的使用者

類的使用者不知道People類內部具體實現,但是可以與之通訊

2

操作系統

Win32 API,如

SetWindowText(hWnd,”text”); //設置某窗口標題

GUI開發者

GUI開發者不知道操作系統內部實現,但是可以與之通訊

3

微博開放平台

https協議url,如加載最新微博

https://api.weibo.com/2/statuses/public_timeliti.json?parameter1=12&parameter2=22

微博第三方應用開發者

微博第三方應用開發者不知道微博服務器內部實現,但是可以與之通訊

4

Google地圖服務

http協議url,如查詢指定城市

地理坐標信息

http://maps.googleapis.com/maps/api/geocode/xml?address=london&sensor=false

地圖第三方應用開發者

地圖第三方應用開發者不知道地圖服務器內部實現,但是可以與之通訊

在.NET編程中,還存在另外一種意義的"接口",即我們使用interface關鍵字定義的接口類型,這種"接口"嚴格意義上講跟我們剛才討論的"接口"不能做相等比較。更准確來說,它代表編程過程中的一種"協議",是代碼中調用方和被調用方必須遵守的契約,如果某一方不遵守,那么調用就不會成功。

    注:有關"協議",請參見下一節。

2.13 協議

協議,即約定、契約。兩個(或兩個以上)個體合作時需要共同遵守的准則,哪一方不遵守該准則,大部分時候將會導致合作失敗,這個是現實生活中我們理解的"協議"。在計算機(編程)世界中,"協議"帶來的效果同樣如此。

計算機網絡通信中,OSI(Open System Interconnection,開放系統互聯模型)將網絡分為7層,每層均有多種協議,通信雙方必須分別遵守各層中對應的協議,如下圖2-11:

圖2-11 網絡七層協議

如上圖2-11所示,數據發送方必須按照規定協議封裝數據,然后才能發送給另一方;同理,數據接收方必須按照對應協議解析接收到的數據包,然后才能獲得發送方發送的原始數據。在實際通信編程中,這些"封裝/解析"的步驟均已被計算機底層模塊完成,因此對用戶來講,這些過程都是透明的,它們一直都在,並且是雙方通信的關鍵。

網絡通信協議是一種數據結構,很多書籍中講到了TCP/UDP協議結構,介紹了協議結構中每(幾)個字節分別代表什么內容,數據發送方按照規定的格式填充該數據結構,數據接收方按照規定的格式去解析該數據結構,從而得到原始數據。不管TCP協議還是UDP協議,均屬於傳輸層協議。對於某些高級語言(如C#、Java)開發者而言,接觸這些協議的機會很少,更多時候,我們接觸的是應用層協議,如HTTP協議、FTP協議等,除了這些主流、廣為人知的協議外,我們自己在開發網絡程序時,也可以自己定義自己的應用層協議,如在編寫雷達航跡顯示系統時,我們可以將接收到的原始雷達數據進行預處理,以某一種預先定義的數據結構(也就是協議)轉發給其他人,其他人按照預先定義好的數據結構(協議)去解析接收到的數據包;還比如在一些即時通信程序中,可能存在"文本消息"、"圖片"、"表情"或者"文件"等一些數據類型,那么我們完全可以定義一個自己的應用層協議,見下圖2-12:

圖2-12 自定義應用層協議

如上圖2-12所示,第一個字節表示消息類型,是文本消息還是表情,可以通過該字節區分,第2~5個字節表示雙方通信次數,第6~9個字節表示"數據區"長度,之后的N個字節表示發送的"原始數據",倒數兩個字節為一些附加數據,最后一個字節為校驗碼,整個數據結構的長度為:(1+4+4+數據區長度+2+1)個字節。發送方填充完整個數據結構,然后發送給接收方,接收方接收到數據后,按照已知的數據結構格式去解析獲得其中的原始數據。發送"文本消息"的示例代碼如下:

 1 //Code 2-11
 2 
 3 public static void SendStringMsg(int sequence, string msg)
 4 {
 5     byte[] msg_buffer = Encoding.Unicode.GetBytes(msg);
 6     byte[] send_buffer = new byte[12 + msg_buffer.Length]; //     NO.1 1 + 4 + 4 + N + 2 + 1
 7     using (MemoryStream ms = new MemoryStream(send_buffer))
 8 {
 9         using (BinaryWriter bw = new BinaryWriter(ms))
10         {
11             bw.Write((byte)1); //NO.2
12             bw.Write(sequence); //NO.3
13             bw.Write(msg_buffer.Length); //NO.4
14             bw.Write(msg_buffer); //NO.5
15             bw.Write((short)0); //NO.6
16             bw.Write((byte)0); //NO.7
17         }
18     }
19     //send 'send_buffer' to receiver with socket... NO.8
20 }        

如上代碼Code 2-11所示,首先定義一個發送緩沖區(NO.1處),因為12個字節已固定,所以緩沖區的長度應該是:12+文本消息長度,然后依次將消息類型(NO.2處)、順序號(NO.3處)、數據長度(NO.4處)、文本消息內容(NO.5處)、附加字(NO.6處)和校驗碼(NO.7處)寫入緩沖區,發送方按照預定義格式填充字節流緩沖區,再將其發送給對方(NO.8處);對應的,接收方接收到數據后,按照預定義格式解析字節流。下圖2-13顯示順序號為10,文本消息為"ABC"時發送緩沖區send_buffer中的內容:

圖2-13 發送緩沖區中的內容

    注:在TCP通訊中,由於數據是以"流"的形式傳遞的,前后發送的數據連接在一起,接收方無法區分單個的消息(找不到消息邊界),若按照上面提到的預先定義一個傳輸協議,接收方可以按照該協議解析出一條完整的消息。詳細參見本書中后續有關"網絡編程"的第九章。

不僅網絡通信需要"協議"的輔助,計算機世界中還有很多場合需要"協議"的輔助,如加密和解密、編碼和解碼以及CPU執行機器指令、計算機通過USB口與外設交換數據等,下面表2-2顯示了各種場合中的"協議":

表2-2 各種場合中的協議

序號

場合

協議

說明

1

加密/解密

使用的同一套算法

加密和解密的算法必須配套,否則會解密失敗

2

編碼/解碼

使用的同一種編碼規范

如各種編碼規范:Unicode、UTF-8、Ascll,編碼和解碼必須使用同一套規范,否則會出現亂碼

3

CPU執行機器指令

CPU和編譯器使用的同一套CPU指令集

CPU和編譯器必須使用同一套指令集,傳統編譯器將高級語言直接編譯成與平台相關的機器碼,機器碼只能在指定平台上運行,CPU和編譯器須遵守同一個規范

4

USB接口

計算機和外設使用的同一種USB規范

計算機與外設必須使用同一種USB規范,如USB1.0、USB1.1或USB2.0,否則兩者之間不能正常交互(不考慮兼容情況)

到目前為止,我們講到的"協議"都能很好地跟現實關聯起來,或者說,它們都跟協議字面意思接近。其實在.NET程序開發過程中,也有一種"協議",它便是使用關鍵字interface聲明的接口。使用interface聲明的接口也是一種"協議",它規定了代碼調用方與代碼被調用方共同遵守的一種規范,前面說過,代碼中Client端與Server端需要交互,那么只有雙方共同遵守某一約定,工作才能正常進行。這種協議在代碼中具體體現在:

    1)調用方必須存在一個接口引用;

    2)被調用方必須實現該接口。

具體示例代碼見Code 2-12:

 1 //Code 2-12
 2 
 3 interface IWalkable //NO.1
 4 {
 5     void Walk();
 6 }
 7 class People:IWalkable //NO.2
 8 {
 9     public void Walk()
10     {
11         //
12     }
13 }
14 class Program
15 {
16     static void Main()
17     {
18         IWalkable w = new People();
19         Func(w);
20     }
21     static void Func(IWalkable w) //NO.3
22     {
23         w.Walk();
24     }
25 }

如上代碼Code 2-12中,NO.1處定義了一個協議(接口),被調用方(NO.2處)遵守了該協議(實現接口),調用方也遵守了該協議(NO.3處,包含一個接口類型參數)。雙方都遵守了同一個協議,才能協調好工作。下圖2-14顯示了"協議"在代碼調用中起到的作用:

圖2-14 代碼調用中的協議

    注:代碼中使用interface聲明的"接口"在面向抽象編程中起到了非常重要的作用,詳細參見本書第十二章。

2.14 本章回顧

本章共介紹了13個將在本書中遇到的概念(術語),或許我們曾經了解過某些概念的含義,但一直處於似懂非懂的狀態,那么閱讀完本章,你肯定會拍下腦袋,高呼:原來是這樣!有些概念在其它地方幾乎找不到准確的解釋,比如"線程和方法的關系"、"庫與框架區別"以及"代碼中的協議"等等;另外一些概念雖然能找到一些解釋說明,但並沒有像本章講得這么詳細。總之,本章定會掃清我們在編程道路上遇見的虐心絆腳石。

2.15 本章思考

1.下面代碼Code 2-13中MyContainer類中的_int_list成員是否是線程安全的,為什么?

 1 //Code 2-13
 2 
 3 class MyContainer
 4 {
 5     List<int> _int_list = new List<int>();
 6     public void Add(int item)
 7     {
 8         _int_list.Add(item);
 9     }
10     public int GetAt(int index)
11     {
12         return _int_list[index];
13     }
14 }

A:不是線程安全的,因為無論是MyContainer.Add()方法還是MyContainer.GetAt()方法,均可以同時在多個線程中運行,這就意味着可能存在多個線程同時訪問集合容器_int_list,可以在MyContainer.Add()以及MyContainer.GetAt()方法中加上鎖(lock(object))來解決該問題。

2.舉例說明實際開發過程中遇見的框架和庫有哪些。

A:框架有:Asp.NET MVC、Asp.NET Webforms、Windows Forms、WCF、WPF以及SilverLight等;庫包括公司內部一些通用庫,如MySQL數據庫訪問工具庫、日志記錄工具庫、字符串處理工具庫、圖片處理工具庫以及加解密工具庫等等。

(本章完)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM