高屋建瓴:梳理编程约定

相关文章连接:

编程之基础:数据类型(一)

编程之基础:数据类型(二)

动力之源:代码中的“泵”

完整目录与前言

高屋建瓴:梳理编程约定

在实际编程中,我们会遇见各种各样的概念,虽然有的并没有官方定义,但是我们可以自己给它取一个形象的名称。本章总结了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数据库访问工具库、日志记录工具库、字符串处理工具库、图片处理工具库以及加解密工具库等等。

(本章完)

 

posted @ 2015-02-03 17:11  周见智  阅读(5507)  评论(15编辑  收藏  举报