2014年6月份自己学习历程

 

 

引 言

考虑到社会的飞速发展和自身能力的不足,实现自己理想的过程是漫长而又艰难的,因此我必须明确这一点:

自己每天必须要有看得见的进步和努力,如果自己这一天玩了,相对于整个社会就是落后了2天,这是一件多么可怕的事。不但要努力进步,而且要比这个社会的发展的速度要快。

因此,我每天将自己学到的东西,包括但不限于个人健康方面、技术方面、商务方面、人际沟通、专业知识方面。如果没有明确的任务,自己必须根据个人实际情况去补充各种知识。否则,你就是虚度光阴,浪费生命了。

 

2014年6月2日星期一

  1. Asp.Net构架之Http请求处理流程
  • Http请求处理流程概述

为什么浏览器输入http://www.microsoft.com/zh-cn/default.aspx就可以看到微软中国的页面,这个就像为什么苹果掉下来为什么落在地上一样。对于普通访问人员来说,都已经习惯了,就像太阳每天都东升西落一样。同样,对于很多程序员来说,认为这个与己无关,这个不过是系统管理员和网管员的责任。毕竟IIS是Windows系统的一个组件,又不是Asp.Net的一部分。而事实上,在你轻按回车到页面出来的这1s之内,IIS和.Net FrameWork已经做了大量的幕后的工作。

作为一个.Net开发者,非常有必要的了解清楚这其中的详细过程和IIS、AspNet、.Net FrameWork三者之间如何相互协作。如工作中,曾经用过HttpContext类,但是却不了解这个类的实体和构成,它是怎么来的,因此必须把这个Http请求处理直至页面出现的过程弄清楚,这样写Web应用程序的时候才有一个底层的认识。

当然,要想回答上面的问题,就必须先知道IIS时怎样处理页面请求的,这也是理解Form验证方式和Windows验证的基础。

  • Http请求刚到达服务器的时候

当服务器接收到一个Http请求时,IIS首先需要决定如何去去处理这个请求(处理一个html页面和一个Aspx页面肯定是不一样的)。那IIS根据什么区请求呢,答案是文件的后缀名

服务器获取所请求的页面(也可以是一个文件,如background.jpg)的后缀名后,接下来会在服务器寻找处理这后缀名的文件的应用程序,如果IIS找不到处理此后缀名的应用程序,并且该文件没有收到服务器的保护(一个受保护的例子App_Code文件夹的文件,一个不受保护的例子是js文件),那么IIS直接将这个文件返还给客户端。

能够处理各种后缀名的应用程序,通常被称作ISAPI应用程序(ISAPI=Internet Server Application Programming Interface,中文叫互联网服务器应用程序接口)。虽然名字很长很霸气,不过是是一个接口而已,起到一个代理的作用,主要功能是映射所请求的页面文件,与此文件后缀名对应的实际处理的应用程序

我们顺便看看ISAPI,看看它到底是什么样子,按下面步骤进行:

  1. 打开IIS
  2. 在主页打开处理程序映射,位置如下所示:

打开后,你看到如下画面:

2014年6月份自己学习历程

可以很清楚的看到,所有IIS能处理或者叫ISAPI所提供代理服务的文件类型及其对应的处理程序都清楚的列出来了。比如.Aspx类型的文件,是通过下面列出的处理程序处理的,如下所示:

2014年6月份自己学习历程

当IIS接收到该类型的文件时,就会将该文件交给ASP.Net处理,因此Asp.Net仅仅是IIS的一个组成部分而已。

这里需要注意两点:

  • 当你修改"限制为"后,可以限制页面(文件)只能以某种特定方式访问
  • "确认文件是否存在"是实现 URL 地址映射的关键选项,我以后会专门讲述。

理解宿主环境(host environment)

从本质上来讲,Asp.Net主要由一系列类组成,这些类的主要目的就是将Http请求变为对于客户端的响应。HttpRunTime类是Asp.Net主要的一个入口,它有一个称作ProcessRequest的方法,这个方法以HttpWorkerRequest类作为参数。HttpRunTime类几乎包含单个Http请求的所有信息:请求的文件、服务器变量、QueryString、Http头信息等等。Asp.Net使用这些信息来加载、运行正确的文件,并将这个请求转换到输出流中,一般来说,也就是HTML页面(也有可能是图片)。

当 Web.config文件的内容发生改变 或者 .aspx文件发生变动的时候,为了能够卸载运行在同一个进程中的应用程序(卸载也是为了重新加载),Http请求被分放在相互隔离的应用程序域中。应用程序域其实就是 AppDomain。

对于IIS来说,它依赖一个叫做 HTTP.SYS 的内置驱动程序来监听来自外部的 HTTP请求。在操作系统启动的时候,IIS首先在HTTP.SYS中注册自己的虚拟路径。这实际上相当于告诉HTTP.SYS哪些URL是可以访问的,哪些是不可以访问的。举个简单的例子:为什么你访问不存在的文件会出现 404 错误呢?就是在这一步确定的。如果请求的是一个可访问的URL,HTTP.SYS会将这个请求交给 IIS 工作者进程(IIS6.0中叫做 w3wp.exe,IIS5.0中叫做 aspnet_wp.exe)。

ISAPI接下来做的事情顺序是这样的:

  1. 映射文件与其对应的处理程序
  2. HTTP.SYS中获取当前的Httq请求信息,并且将这些信息保存到 HttpWorkerRequest 类中。
  3. 在相互隔离的应用程序域AppDomain中加载HttpRuntime
  4. 调用 HttpRuntimeProcessRequest方法。

接下来才是程序员通常编写的代码所完成的工作了,然后,IIS 接收返回的数据流,并重新返还给 HTTP.SYS,最后,HTTP.SYS 再将这些数据返回给客户端浏览器。接下来,就看到微软中国的页面了。

Asp.Net的宿主环境图示如下所示:

2014年6月份自己学习历程

理解管道(Pipeline)

在前面,我们在一个相对比较低的层次上讨论了从发出Http请求到看到浏览器输出这转瞬即逝的时间内IIS和 Framework 所做的事情。但是我们忽略了一个细节:程序员编写的代码是如何在这一过程中衔接的,下面我们就来看看这个问题。

    当Http请求进入 Asp.Net Runtime以后,它的管道由托管模块(Managed Modules)和处理程序(Handlers)组成,并且由管道来处理这个 Http请求。

Http管道图示如下:

2014年6月份自己学习历程

我们按编号来看一下这幅图中的数据是如何流动的。

1. HttpRuntime将Http请求转交给 HttpApplication,HttpApplication代表着程序员创建的Web应用程序。HttpApplication创建针对此Http请求的 HttpContext对象,这些对象包含了关于此请求的诸多其他对象,主要是HttpRequest、HttpResponse、HttpSessionState等。这些对象在程序中可以通过Page类或者Context类进行访问。、

2. 接下来Http请求通过一系列Module,这些Module对Http请求具有完全的控制权。这些Module可以做一些执行某个实际工作前的事情。

3. Http请求经过所有的Module之后,它会被HttpHandler处理。在这一步,执行实际的一些操作,通常也就是.aspx页面所完成的业务逻辑。可能你会觉得在创建.aspx页面并没有体会到这一过程,但是,你一定知道,.aspx 页面继承自Page类,我们看一下Page类的签名:

可以看到,Page类实现了IHttpHandler接口,HttpHandler也是Http请求处理的最底层。

4.HttpHandler处理完以后,Http请求再一次回到Module,此时Module可以做一些某个工作已经完成了之后的事情

如果我们将注意力只集中在Http请求、HttpHandler和HttpModule上,不去考虑HttpContext和HttpApplication,那么Http请求在HttpHandler 和 HttpModule 中的流动方向可以简化成下面这样:

2014年6月份自己学习历程

上面主要讲解了3个方面的内容:

  • Http请求刚刚到达时IIS时,IIS 所做的工作。
  • Http请求的宿主环境。
  • Http管道。

 

2014年6月3日星期二

  1. Asp.Net构架之Http Handler介绍

上面我们了解了Http的请求处理过程以及其他的基本原理。我们知道Http管道中有两个可用的接口:一个是IHttpHander,另一个是IHttpModule。但是我们并不知道怎么使用这两个可用的接口进行编程。在本文中,我们将在实际例子中来学习在编程中如何使用IHttpHander。

而HttpHandler是一个HTTP请求的真正处理中心,也正是在这个HttpHandler容器中,ASP.NET Framework才真正地对客户端请求的服务器页面做出编译和执行,并将处理过后的信息附加在HTTP请求信息流中再次返回到HttpModule中。

  • IHttpHander概述

很多Asp.Net开发人员都有过Asp的背景,以至于在开发程序的时候,通常都是在"页面级"上思考,也就是说我们现在正在做的这个页面应该有什么样的功能,是进行一个问卷调查还是一个数据库查询等等。而很少在"请求级"思考,考虑有没有办法来通过编码的方式来操控一个Http请求。

实际上,Framework提供了一系列的接口和类,允许你对于Http请求进行编程,而实现这一操作的一个主要的接口,就是 IHttpHandler(另一个是IHttpModule)。

应该还记得第一节中我们提到过 ISAPI,它根据文件名后缀把不同的请求转交给不同的处理程序。但是仔细看看就会发现:几乎一大半的文件都交给 aspnet_isapi.dll 去处理了。很明显,aspnet_isapi.dll 不可能对每种文件采用同一种方式处理,那么 aspnet_isapi.dll 是如何更进一步处理不同的文件,交由谁去处理呢,为搞清楚这个问题,打开C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG\ 目录下的web.config 文件。找到<httpHandlers>节点,可以看到如下所示代码:

2014年6月份自己学习历程

可以看到,在<httpHandlers>结点中将不同的文件类型映射给不同的Handler去处理,对于.aspx来说,是由System.Web.UI.PageHandlerFactory来处理。而对于.cs来说,是由System.Web.HttpForbiddenHandler处理,从ForbiddenHandler名字中出现的Forbidden (翻译过来是"禁止")可以看出,这个Handler可以避免我们的源码被看到。

注意:System.Web.UI.PageHandlerFactory 是一个IHttpHandlerFactory,而不是一个单一的HttpHandler,IHttpHandlerFactory用来做什么后面会说明

System.Web.UI.PageHandlerFactory 是一个IHttpHandlerFactory,而不是一个单一的HttpHandler,IHttpHandlerFactory用来做什么后面会说明。

IHttpHandler的定义是这样的:

2014年6月份自己学习历程

 

由上面可以看出IHttpHandler要求实现一个方法和一个属性。其中 ProcessRequest,从名字(处理请求)看就知道这里应该放置我们处理请求的主要代码。

IsReusable属性,MSDN上是这样解释的:获取一个值,该值指示其他请求是否可以使用 IHttpHandler 实例。也就是说后继的Http请求是不是可以继续使用实现了该接口的类的实例,一般来说,我把它设置成true。

那么实现此接口的类形式应该是这样的:

2014年6月份自己学习历程

实际的例子中可能会在这样写,如前段时间写的ZTree树形树的Handler类如下:

2014年6月份自己学习历程

大概的功能是获取浏览器请求的ID,根据ID通过数据库查询对应ID下的下属单位,然后将查询到的数据拼接成Json数据格式返回给浏览器。

而为了能使用这个自定义的HttpHandler,我们需要在应用程序目录下的Web.config中注册它,如下所示:

<system.web>

<httpHandlers>

<add path="*.jpg" verb="*" type="MyNameSpace.MyClass, MyDllName" />

</httpHandlers>

</system.web>

应该发现这与之前在C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG\目录下web.cofig中看到的几乎完全一样。这里,path指的是请求的文件名称,可以使用通配符扩大范围,也可以明确指定这个handler仅用于处理某个特定的文件(比如说:filename.aspx)的请求。verb指的是请求此文件的方式,可以是post或get,用*代表所有访问方式。type属性由","分隔成两部分,第一部分是实现了接口的类名,第二部分是位于Bin目录下的编译过的程序集名称。

注意:

  1. 如果你新建一个项目,并且在项目下创建HandlerTest.cs,然后让站点引用该项目,那么在生成解决方案的时候会自动将编译好的.dll文件添到Bin目录中。
  2. MyDll只写程序集名,不要加后面的.dll
  • IHttpHandler如何处理HTTP请求

当一个HTTP请求经同HttpModule容器传递到HttpHandler容器中时,ASP.NET Framework会调用HttpHandler的ProcessRequest成员方法来对这个HTTP请求进行真正的处理。以一个ASPX页面为例,正是在这里一个ASPX页面才被系统处理解析,并将处理完成的结果继续经由HttpModule传递下去,直至到达客户端。

对于ASPX页面,ASP.NET Framework在默认情况下是交给System.Web.UI.PageHandlerFactory这个HttpHandlerFactory来处理的。所谓一个HttpHandlerFactory,是指当一个HTTP请求到达这个HttpHandler Factory时,HttpHandlerFactory会提供出一个HttpHandler容器,交由这个HttpHandler容器来处理这个HTTP请求。

一个HTTP请求都是最终交给一个HttpHandler容器中的ProcessRequest方法来处理的

如下图所示:

2014年6月份自己学习历程

实例1:通过IhttpHandler实现图片验证码

打开VS创建一个空的Web项目,点击"添加新项"选择" 一般处理程序",生成的代码如下所示:

2014年6月份自己学习历程

为生成验证码,将代码修改如下:

2014年6月份自己学习历程

新建一个新页面Default.Aspx,代码如下所示:

在浏览器查看该页面,如下所示:

2014年6月份自己学习历程

如果上面HttpHander类没有继承IRequiresSessionState接口,在类中调用了如下代码:

会提示报错,如下所示:

2014年6月4日星期三

  1. Asp.Net构架之Http Module介绍

上面已经讲解了HTTP请求处理的流程和HttpHandler,了解了Http请求在服务器端的处理流程,随后我们知道Http请求最终会由实现了IHttpHandler接口的类进行处理(应该记得Page类实现了IHttpHandler)。从 Http 请求处理流程一文中可以看到,在Http请求由IHttpHandler处理之前,它需要通过一系列的Http Module;在请求处理之后,它需要再次通过一系列的Http Module,那么这些Http Module是如何组成的?用来做什么呢?本文将对Http Module作以介绍。

  • Http Module概述

暂时先把考虑我们自己实现Http Module的情况。在.Net中,Http Module是实现了IHttpModule接口的程序集,而IHttpModule接口本没什么好大写特写的。由名字可以看出,这仅仅只是一个接口而已。实际上,我们关心的是实现了这个接口的类,如果我们编写了代码实现了这个接口,会有什么用途。一般来说,将Asp.Net的事件分为3个级别:最顶层是应用程序事件、其次是页面事件、最后是控件事件,事件的触发分别与应用程序周期、页面周期、控件周期密切相关。而Http Module是与应用程序事件紧密相关的

我们通过Http Module在Http请求管道(Pipeline)中注册期望对应用程序事件做出反应的方法,在相应的事件触发的时候(比如说BeginRequest事件,它在应用程序收到一个Http请求并即将对其进行处理时触发),便会调用Http Module注册了的方法,实际的工作在这些方法中执行。.Net 本身已经有很多的Http Module,其中包括 表单验证Module(FormsAuthenticationModule), Session 状态Module(SessionStateModule),输出缓存Module (OutputCacheModule)等。

  • 注册Http Module

在注册我们自己编写的 Http Module 之前,先来看看Asp.Net中已经有的HttpModule。与 Http Handler类似,我们需要打开机器上C:\WINDOWS\Microsoft.NET\Framework\ v2.0.50727\CONFIG 目录下的 web.config 文件。找到 <httpModules/> 结点,应该可以看到下面的内容:

2014年6月份自己学习历程

用表格表示该节点中含有的Http Module:

2014年6月份自己学习历程

我们先从结点上看,type属性与上一节所说的http handler结点的type属性类似,都代表了相应的程序集。但是,与http handler 不同,module只提供了一个name属性,没有诸如 path这样指定某一特定(或者用通配符 * 代表某一种类)文件的处理程序。这是与Module的特点相关的,我们知道 module 是响应应用程序周期中触发的事件,对于所有提交到aspnet_isapi.dll的请求都一样,即便请求只是像类似http://www.baidu/images/logo.gif 这样获取一张图片而已(对ISAPI进行设置以后,默认aspnet_isapi.dll不接手图片文件)。

与Http handler类似,在这册我们自己的http module 时,假设类名为ModuleDemo,位于myNameSpace命名空间下,程序集名称为myDll,我们只需将myDll.dll拷贝到Bin目录下,并在站点的 web.config 文件 system.web 结点下创建 httpModules 结点:

type属性由分号","分为两部分,前面是命名空间及类名,也就是类型名;后面是程序集名。如果我们将代码创建在App_Code目录中,则不需要再指定程序集名。

name属性由我们自己命名,不一定与类名相同,此处我将它命名为"CustomModuleName"。我们可以通过应用程序(HttpApplication)的Modules属性获取HttpModuleCollection集合,然后通过name属性,进一步获取HttpModule对象。

通过name属性,我们还可以在global.asax中文件中编写自定义HttpModule暴露出的事件的处理程序,它采用的格式是:void ModuleName_EventName(object sender, EventArgs e)。我们将在后面做更详细介绍。

  • IHttpModule接口

IHttpModule 接口,它包括下面两个方法:

Init():这个方法接受一个HttpApplication对象,HttpApplication代表了当前的应用程序,我们需要在这个方法内注册 HttpApplication对象暴露给客户端的事件。可见,这个方法仅仅是用来对事件进行注册,而实际的事件处理程序,需要我们另外写方法。

整个过程很好理解:

  1. 当站点第一个资源被访问的时候,Asp.Net会创建HttpApplication类的实例,它代表着站点应用程序,同时会创建所有在Web.Config中注册过的Module实例。
  2. 在创建Module实例的时候会调用ModuleInit()方法。
  3. Init()方法内,对想要作出响应的HttpApplication暴露出的事件进行注册。(仅仅进行方法的简单注册,实际的方法需要另写)
  4. HttpApplication在其应用程序周期中触发各类事件。
  5. 触发事件的时候调用Module在其Init()方法中注册过的方法。

Dispose():它可以在进行垃圾回收之前进行一些清理工作。

综上所述实现IHttpModule一个模板是这样的:

2014年6月份自己学习历程

  • 通过Http ModuleHttp请求输出流中写入文字

本例中,我们仅用BeginRequest事件和 EndRequest 事件对 Http Module 的使用作以说明。我们通过这个范例,了解 Http Module 基本的使用方法。添加"新建项",选择Asp.Net模块,如下所示:

2014年6月份自己学习历程

代码自动实现IHttpModule接口,具体代码如下所示:

2014年6月份自己学习历程

上面的代码很简单,它注册了 HttpApplication实例的 BeginRequest 事件 和 EndRequest事件,事件处理方法的作用仅仅是在http请求开始和结束的时候,给http请求的输入流中分别写入不同的内容。在默认页(Default.Aspx页面)查看,如下所示:

2014年6月份自己学习历程

在本工程上继续添加一个Test.Aspx页面,如下所示:

2014年6月份自己学习历程

查看本页面运行画面,如下所示:

2014年6月份自己学习历程

可以看到,两个页面的效果相同。这说明对于不同的两个文件,Http Module都起了作用,可见它确实是位于应用程序级,而非页面级

  • Global.asax文件与 Http Module

早在asp时代,大家就知道这个文件了。它主要用于放置对于 应用程序事件或者 Session事件的响应程序。大家熟悉的有Application_Start、Application_End、Session_Start、Session_End 等。

在asp.net中,Glabal不仅可以注册应用程序和Session事件,还可以注册Http Module暴露出的事件;不仅可以注册系统Module的事件,也可以注册我们自己义的Module暴露出的事件。在具体介绍之前,这里需要首先注意两点:

  1. 在每处理一个Http请求时,应用程序事件都会触发一遍,但是Application_Start Application_End 例外,它仅在第一个资源文件被访问时被触发。
  2. Http Module无法注册和响应Session事件,对于Session_Start Session_End,只能通过Glabal.asax来处理。

接下来,我们在站点中创建一个 Global.asax 文件,在里面添加如下代码,注意到格式是:void 模块名_事件名(object sender, EventArgs e)。现在,我们打开之前的页面,可见,我们成功的将 Glabal.asax文件与我们自己定义的Http Module所暴露出的事件 ExposedEvent 联系到了一起。这就是Global.asax文件与Http Module的关系。

 

2014年6月5日星期四

  1. WebService和AngularJS实现模糊过滤查询

网上看到一个用WebService获取json,然后在前端使用AngularJs进行过滤搜索,看完文章后,感觉整个功能挺实用,于是整理了一下便于理解WebService。

WebService:是一个平台独立的,低耦合的,自包含的、基于可编程的web的应用程序,可使用开放的XML(标准通用标记语言下的一个子集)标准来描述、发布、发现、协调和配置这些应用程序,用于开发分布式的互操作的应用程序。

AngularJS:通过为开发者呈现一个更高层次的抽象来简化应用的开发。如同其他的抽象技术一样,这也会损失一部分灵活性。换句话说,并不是所有的应用都适合用AngularJS来做。AngularJS主要考虑的是构建CRUD应用。幸运的是,至少90%的WEB应用都是CRUD应用。但是要了解什么适合用AngularJS构建,就得了解什么不适合用AngularJS构建。

例子执行成功后,默认显示所有的用户列表,如下所示:

 

输入框中输入"王",结果如下:

这样就简单实现了模糊查询。下面简单说一下实现过程:

  1. 选用数据库,这个用哪个无所谓,只要取得DataTable即可。为方便,这里使用的是Access,表结构如下:

    2014年6月份自己学习历程

  2. 通过VS创建一个Web应用程序,然后添加Default.aspxangular.min.jsWebService.asmx,结构如下所示:

    2014年6月份自己学习历程

  3. 编写BLL层,主要完成数据库的读取和提供Json功能

    读取数据库:

    2014年6月份自己学习历程

    DataTable转换为Json数据:

    2014年6月份自己学习历程

  4. 编写WebService

    这里编写一个HelloWorld()方法,用于提供json数据,代码如下:

    2014年6月份自己学习历程

    Web.Config文件中system.web节点中添加WebService协议如点,代码如下:

    2014年6月份自己学习历程

  5. 这时访问WebService下的HelloWorld()方法,得到如图:

    2014年6月份自己学习历程

  6. 编写Html,在Default.Aspx页面中引用AngularJS,代码如下所示:

    2014年6月份自己学习历程

2014年6月6日星期五

  1. Http协议详解

当今Web开发技术真可谓是百家齐放,ASP.NET, PHP, JSP,Perl, AJAX 等等。 无论Web技术在未来如何发展,理解Web程序之间通信的基本协议相当重要,因为它让我们理解了Web应用程序的内部工作,结合Fiddler这个Web调试工具来一起理解Http协议。

  1. Http协议概念

协议是指计算机通信网络中两台计算机之间进行通信所必须共同遵守的规定或规则,超文本传输协议(HTTP)是一种通信协议,它允许将超文本标记语言(HTML)文档从Web服务器传送到客户端的浏览器。目前我们使用的是HTTP/1.1 版本。

  1. Web服务器,浏览器,代理服务器

当我们打开浏览器,在地址栏中输入URL,然后我们就看到了网页。 原理是怎样的呢?实际上我们输入URL后,我们的浏览器给Web服务器发送了一个Request, Web服务器接到Request后进行处理,生成相应的Response,然后发送给浏览器, 浏览器解析Response中的HTML,这样我们就看到了网页,过程如下图所示:

2014年6月份自己学习历程

当然,浏览器发出的Request也有可能时经过了代理服务器(Proxy),最后才到达服务器的:

2014年6月份自己学习历程

所谓的代理,顾名思义就是网络一个中转站,有什么具体功能呢?

  • 提高访问速度,大多数的代理服务器都有缓存功能。
  • 突破限制,也就是FQ了
  • 隐藏身份。
  1. URL详解

    URL(Uniform Resource Locator,即为统一资源定位器) 地址用于描述一个网络上的资源, 基本格式如下:

    schema://host[:port#]/path/.../[?query-string][#anchor]

        scheme 指定低层使用的协议(例如:http, https, ftp)

host HTTP服务器的IP地址或者域名

port# HTTP服务器的默认端口是80,此时端口号可以省略。如果使用了别的端口,必须指明,例如 http://www.cnblogs.com:8080/

path 访问资源的路径

query-string 发送给http服务器的数据

anchor- 锚

示例如下所示:

http://www.mywebsite.com/sj/test/test.aspx?name=sviergn&x=true#stuff

Schema: http

host: www.mywebsite.com

path: /sj/test/test.aspx

Query String: name=sviergn&x=true

Anchor: stuff

  1.     HTTP协议是无状态的

Http协议时无状态的,对于同一个客户端来说,这一次请求和上一次请求没有对应关系。Http服务器不知道这两个请求来自同一个客户端。为了解决这个问题,因此Web程序引入了Cookie机制来维护状态。

  1. 打开一个网页需要浏览器发送多次Request
  • 当你在浏览器输入URL http://www.baidu.com 的时候,浏览器发送一个Request去获取 http://www. baidu.com html. 服务器把Response发送回给浏览器.
  • 浏览器分析Response中的 HTML,发现其中引用了很多其他文件,比如图片,CSS文件,JS文件。
  • 浏览器会自动再次发送Request去获取图片,CSS文件,或者JS文件。
  • 等所有的文件都下载成功后。网页就被显示出来了。

具体如下图所示:

  1. HTTP消息的结构

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据4个部分组成,下图给出了请求报文的一般格式。:

2014年6月份自己学习历程

第一行中的Method表示请求方法,比如"POST","GET", Path-to-resoure表示请求的资源, Http/version-number 表示HTTP协议的版本号。

如请求百度的完整Request如下所示:

 

当使用的是"GET" 方法的时候, body是为空的

  1. GetPost方法的区别

Http协议定义了很多与服务器交互的方法,最基本的有4种,分别是GET,POST,PUT,DELETE. 一个URL地址用于描述一个网络上的资源,而HTTP中的GET, POST, PUT, DELETE就对应着对这个资源的查,改,增,删4个操作。 我们最常见的就是GET和POST了。GET一般用于获取/查询资源信息,而POST一般用于更新资源信息

我们看看GET和POST的区别

1. GET提交的数据会放在URL之后,以?分割URL和传输数据,参数之间以&相连,如EditPosts.Aspx?name=test1&id=123456.  POST方法是把提交的数据放在HTTP包的Body中.

2. GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制.

3. GET方式需要使用Request.QueryString来取得变量的值,而POST方式通过Request.Form来获取变量的值。

4. GET方式提交数据,会带来安全问题,比如一个登录页面,通过GET方式提交数据时,用户名和密码将出现在URL上,如果页面可以被缓存或者其他人可以访问这台机器,就可以从历史记录获得该用户的账号和密码.

  1. 状态码

Response 消息中的第一行叫做状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。状态码用来告诉HTTP客户端,HTTP服务器是否产生了预期的Response.

HTTP/1.1中定义了5类状态码, 状态码由三位数字组成,第一个数字定义了响应的类别

  • 1XX 提示信息 - 表示请求已被成功接收,继续处理;
  • 2XX 成功 - 表示请求已被成功接收,理解,接受;
  • 3XX 重定向 - 要完成请求必须进行更进一步的处理;
  • 4XX 客户端错误 - 请求有语法错误或请求无法实现;
  • 5XX 服务器端错误 - 服务器未能实现合法的请求。

常见的状态码:

200 ok

这是最常见的状态码了,表示请求被完成,所请求的资源也全部响应。如下所示:

2014年6月份自己学习历程

 

 

2014年6月9日星期一

  1. 委托和方法的异步调用

在通常的情况下,如果需要异步执行一个耗时的操作,就会新建一个线程,然后让这个线程执行相关的代码。但是对于每一个异步调用都通过创建线程来进行操作显然会对性能产生一定的影响,同时操作也会相对繁琐一些。在.Net中可以通过委托进行方法异步的调用,就是说客户端在异步调用方法时,本身并不会因为方法的调用而中断,而是从线程池中抓取一个线程执行该操作,自身线程(主线程)在完成抓取线程操作后会继续向下执行,这样就实现了代码的并行执行。使用线程池的好处就是避免了频繁地进行异步调用时创建、销毁线程。

当在委托对象时调用BeginInvoke()方法时,便进行了一个异步的调用。使用异步调用更多的是为了提升系统的性能,而非专门用于事件的发布和订阅这一编程模型。而在这种情况下使用异步编程时,就需要更多的控制,比如异步方法调用结束时通知客户端、返回异步调用执行方法的返回值等。下面就对BeginInvoke()、EndInvoke()方法和其相关的IAsyncResult做一个简单的了解。

下面是不使用异步调用的情况,代码如下所示:

2014年6月份自己学习历程

2014年6月份自己学习历程

运行程序,结果如下所示:

2014年6月份自己学习历程

可以看出上面的程序完全就是串行执行的,因为整个程序都在主线程执行,同时通过程序执行的时间也可以看出。

下面将原来的程序改为使用委托异步调用的方式。定义一个AddDelegate委托并使用BeginInvoke()方法调用。BeginInvoke()方法除了最后两个参数AsyncCallBack类型和Object类型以外,前面的参数类型和个数与委托定义一样。另外,BeginInvoke()方法返回了一个实现了IAsyncResult接口的AsyncResult类型实例。

AsyncResult类型的用途如下所示:

  • 传递参数,它包含了对调用了BeginInvoke()方法委托的引用。
  • 包含了BeginInvoke()方法的最后一个object类型的参数。
  • 它可以鉴别出是哪个方法的哪一次调用,因为通过同一个委托变量可以多次调用同一个方法。

EndInvoke()方法接受IAsyncResult类型的对象(以及ref、out类型参数,这里不做详解),所以在调用BeginInvoke()方法是需要保留其返回值以便在调用EndInvoke()时进行传递。最重要的是这里EndInvoke()方法的返回值就是所异步调用方法的返回值。当客户端EndInvoke()方法时只有在BeginInvoke()方法调用完成时才会调用,否则会中断当前线程等待该方法。上面的代码改为异步调用如下所示:

2014年6月份自己学习历程

程序运行结果如下所示:

2014年6月份自己学习历程

有的可能会将获得返回值的操作放在另一段代码或者客户端去执行,而不是像上面一样直接写在BeginInvoke()方法的后面。BeginInvoke()方法的最后两个参数分别是AsyncCallBack类型和Object类型,其中AsyncCallBack类型是一个委托类型,用于方法的回调,也就是异步调用结束时自动调用的方法。它的定义为:

    Public delegate void AsyncCallBack(IAsyncResult ar);

    Object类型可以传递任何想要的数据类型,他可以通过AsyncResult的AsyncState属性获得。下面的代码将获取返回值、打印返回值的操作放到了OnAddComplete()回调方法中。回调方法注意参数是IAsyncResult,代码如下:

2014年6月份自己学习历程

主函数代码如下所示:

2014年6月份自己学习历程

运行结果如下所示:

2014年6月份自己学习历程

 

2014年6月10日星期二

  1. C#异步传输字符串

消息发送时的问题

这个问题就是:客户端分两次向流中写入数据(比如字符串)时,我们主观上将这两次写入视为两次请求;然而服务端有可能将这两次合起来视为一条请求,这在两个请求间隔时间比较短的情况下尤其如此。同样,也有可能客户端发出一条请求,但是服务端将其视为两条请求处理。下面列出了可能的情况,假设我们在客户端连续发送两条"Welcome to Tracefact.net!",则数据到达服务端时可能有这样三种情况:

注意:在这里我们假设采用ASCII编码方式,因为此时上面的一个方框正好代表一个字节,而字符串到达末尾后为持续的0(因为byte是值类型,且最小为0)。

上面的第一种情况是最理想的情况,此时两条消息被视为两个独立请求由服务端完整地接收。第二种情况的示意图如下,此时一条消息被当作两条消息接收了:

而对于第三种情况,则是两条消息被合并成了一条接收:

下面是发送字符串的代码:

2014年6月份自己学习历程

运行服务端,然后再运行这个客户端,你可能会看到这样的结果:

可以看到,尽管上面将消息分成了三条单独发送,但是服务端却将后两条合并成了一条。对于这些情况,我们可以这样处理:就好像HTTP协议一样,在实际的请求和应答内容之前包含了HTTP头,其中是一些与请求相关的信息。我们也可以订立自己的协议,来解决这个问题,比如说,对于上面的情况,我们就可以定义这样一个协议:

[length=XXX]:其中xxx是实际发送的字符串长度(注意不是字节数组buffer的长度),那么对于上面的请求,则我们发送的数据为:"[length=25]Welcome to TraceFact.Net!"。而服务端接收字符串之后,首先读取这个"元数据"的内容,然后再根据"元数据"内容来读取实际的数据,它可能有下面这样两种情况:

  1. "[""]"中括号是完整的,可以读取到length的字节数。然后根据这个数值与后面的字符串长度相比,如果相等,则说明发来了一条完整信息;如果多了,那么说明接收的字节数多了,取出合适的长度,并将剩余的进行缓存;如果少了,说明接收的不够,那么将收到的进行一个缓存,等待下次请求,然后将两条合并。
  2. "[""]"中括号本身就不完整,此时读不到length的值,因为中括号里的内容被截断了,那么将读到的数据进行缓存,等待读取下次发送来的数据,然后将两次合并之后再按上面的方式进行处理。

接下来我们来看下如何来进行实际的操作,实际上,这个问题已经不属于C#网络编程的内容了,而完全是对字符串的处理。所以我们不再编写服务端/客户端代码,直接编写处理这几种情况的方法:

2014年6月份自己学习历程

2014年6月份自己学习历程

这个方法接收一个满足协议格式要求的输入字符串,然后返回一个数组,这是因为如果出现多次请求合并成一个发送过来的情况,那么就将它们全部返回。随后简单起见,我在这个类中添加了一个静态的Test()方法和PrintOutput()帮助方法,进行了一个简单的测试,注意我直接输入了length=13,这个是我提前计算好的。代码如下所示:

2014年6月份自己学习历程

2014年6月份自己学习历程

具体全部代码如下所示:

 

测试结果如下:

2014年6月份自己学习历程

OK,从上面的输出可以看到,这个方法能够满足我们的要求。最开始提出的问题,可以很轻松地通过加入这个方法来解决,这里就不再演示了,但在本文所附带的源代码含有修改过的程序。在这里花费了很长的时间,接下来让我们回到正题,看下如何使用异步方式完成上一篇中的程序吧。

异步传输字符串

服务器与客户端通信由简到繁,有四种方式:服务一个客户端的一个请求、服务一个客户端的多个请求、服务多个客户端的一个请求、服务多个客户端的多个请求。我们说到可以将里层的while循环交给一个新建的线程去让它来完成。除了这种方式以外,我们还可以使用一种更好的方式――使用线程池中的线程来完成。我们可以使用BeginRead()、BeginWrite()等异步方法,同时让这BeginRead()方法和它的回调方法形成一个类似于while的无限循环:首先在第一层循环中,接收到一个客户端后,调用BeginRead(),然后为该方法提供一个读取完成后的回调方法,然后在回调方法中对收到的字符进行处理,随后在回调方法中接着调用BeginRead()方法,并传入回调方法本身。

  • 服务端的实现

当程序越来越复杂的时候,就需要越来越高的抽象,所以从现在起我们不再把所有的代码全部都扔进Main()里,这次我创建了一个Server类,它对于服务端获取到的TcpClient进行了一个包装:

2014年6月份自己学习历程

2014年6月份自己学习历程

随后,我们在主程序中仅仅创建TcpListener类型实例,由于RemoteClient类在构造函数中已经完成了初始化的工作,所以我们在下面的while循环中我们甚至不需要调用任何方法:

2014年6月份自己学习历程

服务端代码,如下所示:

好了,服务端的实现现在就完成了,接下来我们再看一下客户端的实现。

 

  • 客户端的实现

与服务端类似,我们首先对TcpClient进行一个简单的包装,使它的使用更加方便一些,因为它是服务端的客户,所以我们将类的名称命名为Client:

2014年6月份自己学习历程

2014年6月份自己学习历程

2014年6月份自己学习历程

在上面的SendMessage()方法中,我们让它连续发送了三条同样的消息,这么仅仅是为了测试,因为异步操作同样会出现上面说过的:服务器将客户端的请求拆开了的情况。最后我们在Main()方法中创建这个类型的实例,然后调用SendMessage()方法进行测试:

2014年6月份自己学习历程

是不是感觉很清爽?因为良好的代码重构,使得程序在复杂程度提高的情况下依然可以在一定程度上保持良好的阅读性。先后运行服务端和客户端(运行3个客户端)。服务端如下所示:

2014年6月份自己学习历程

3个客户端如下所示:

2014年6月份自己学习历程

2014年6月份自己学习历程

2014年6月份自己学习历程

 

2014年6月11日星期三

  1. byte[]数组与对应类型转换

使用Bitconverter自带的静态方法可以非常简单的获取某一类型对应的byte数组或者将byte数组转化为对应类型。如下所示:

2014年6月份自己学习历程

另外一个方法是使用Marshal来实现,Marshal类包含了内存操作的大部分方法,如空间的分配,指针,内存复制,内存读写等。下面是一段实力程序,如下所示:

2014年6月份自己学习历程

    加断点调试并监视相关变量,如下所示:

2014年6月份自己学习历程

仔细分析,不难发现1234567对应16进制数是0x0012D687与对应的byte数组array,由4个字节组成:

array[0]=135(0x87);

array[1]=214(0xD6);

array[2]=18(0x12);

array[3]=0(0x00);

综上所述,整型数转化为的数组一定是低位在前,而低位在前的数组才能直接转换为对应整型数。看来使用Bitconverter方法是最简单的,远比下面的要直接:

以后写程序,不光要写出来,而是要以最简单最直接最不容易犯错的方式写出来。在这里,提醒自己要注意多多练习Linq,消灭不必要的for循环,当然干掉for循环不太可能。

 

2014年6月12日星期四

  1. Javascript基础回顾
  • Javascriptjava没有任何的关系,这点必须要明确哈
  • Javascript是区分大小写的
  • isNaN()是判断变量是否为一个数字的内置方法
  • 如果字符串内使用的引号和包围字符串的引号一致,则必须使用注意转义字符。
  • var str=''This is a string';str.substring(0,5)的结果是'This ',substring方法返回第一个参数到第二个参数(不包括第二个参数)的字符串。
  • null与空值的区别。Null表示没有,表示并等于false
  • 对象使用花括号创建。下面就创建了一个obj的空对象,var obj={};带有属性的对象,如下所示:var dataLog={"id"=""1","name"=""JXJ"};
  • Date对象使用注意:

    2014年6月份自己学习历程

  • Math对象的使用,注意如下:

    2014年6月份自己学习历程

  • 运算符两端变量类型不一致时,Javascript在执行加法(或任何数学运算时)都必须将

    其中的一个类型转换,如下所示:

    2014年6月份自己学习历程

  • 注意相等操作符==!====!==的使用方法,如下所示:

    2014年6月份自己学习历程

  • In操作符和instanceof操作符如下所示:

    Name没加引号

    2014年6月份自己学习历程

    Name加了引号

    2014年6月份自己学习历程

  • 操作符对应于操作数的位置决定了代码的返回值,当操作符在变量的后面(称之为后缀)时,操作符在(自增或自减)之前返回值,如下所示:

    2014年6月份自己学习历程

相反,当操作符在变量的前面(称之为前缀)时,操作符在(自增或自减)之后返回值,如下所示:

2014年6月份自己学习历程

  • 使用typeof返回返回变量类型,如下所示:

    2014年6月份自己学习历程

  • 三元条件语句,如下所示:

  • 调用函数时没加圆括号,后果是很严重的,此时函数不会被调用,返回函数本身的定义,如下所示:

    2014年6月份自己学习历程

2014年6月13日星期五

  1. SubSonic 3.0介绍

SubSonic是Rob Conery用c#语言写的一 个ORM开源框架,使用BSD软件授权许可(The BSD 3-Clause License)。它是一个实用的快速开发框架,通过非常简单的配置,以及附带的T4模板,就可以帮我们生成功能强大的数据访问层工具,让开发人员远离SQL语句的拼接,专注于业务逻辑的开发。

     SubSonic3.0适合短、平、快的中小型项目开发,支持MsSql、MySql与SQLite三种数据库,支持Linq和支持事务。对数据库操作灵活,对生成的SQL语句会自动进行优化,虽然是ORM框架,但在性能上并没有太大的损失。它上手容易,是一个非常棒的ORM开发框架。

 3.0版本最大的缺点是框架未开发完善,对多表关联查询支持的不是很好,只支持一个多表关联条件(复杂的多表关联只能使用存储过程或SQL语句拼接方式来实现);条件语句对In与Not In不支持(需要在数据层重新封装处理才行);如果使用Redis缓存的话,存储数据时对subsonic3.0生成的实体有兼容问题,需要做一次转换封装后才能使用。

不过总的来说,这些缺点都是可以克服的,在开发效率、二次开发、软件维护上来说,优点也是非常明显的,请大家耐心学完本系列,你就能很容易的掌握SubSonic3.0这个开发利器。

  1. SubSonic 3.0安装说明

下面是下载的相关练习附件:

2014年6月份自己学习历程

在MsSql中新建一个数据库SubSonicTest,如下所示:

2014年6月份自己学习历程

为SubSonicTest数据库设置好登陆帐号与密码为SubSonicTest,如下所示:

2014年6月份自己学习历程

然后为SubSonicTest用户名赋予对SubSonicTest数据库的各种权限,此处直接参考sa用户的设置,使得SubSonicTest用户具有sa相同的权限。

使用SQLDBX连接到SubSonicTest数据库(当然用户名和密码也是SubSonicTest),如下所示:

2014年6月份自己学习历程

然后执行附件中SQL语句.txt文件中的SQl语句,可以看到数据库SubSonicTest有了两张表和一个存储过程,如下所示:

2014年6月份自己学习历程

打开VS,创建一个空白的解决方案,如下所示:

2014年6月份自己学习历程

在该空白解决方案创建一个空的Web应用程序项目SubSonicTest,将SubSonic文件夹复制至项目文件夹中,如下所示:

2014年6月份自己学习历程

打开Web.config,添加数据库链接字串,如下所示:

将Dll和SubSonic.Core复制到解决方案中,如下所示:

2014年6月份自己学习历程

添加SubSonic3.0项目(直接使用源码项目,方便在使用时调试及修改),如下所示:

2014年6月份自己学习历程

添加SubSonic.Core项目和Castle.Core.dll引用,如下所示:

2014年6月份自己学习历程

将SubSonic包含进项目中(右键选择包括在项目中)。而SubSonic文件夹中的文件就是著名的T4模板,如下所示:

2014年6月份自己学习历程

与数据库连接相关的部分在Settings.ttinclude配置文件中,打开该文件,如下所示:

2014年6月份自己学习历程

可以看到上图最后的部分中的const string ConnectionStringName = "procom";其实就是数据库连接字符串名称;const string DatabaseName = "SubSonicTest";就是代表数据库的名称。设置好以后,选择全部T4模板文件,运行生成代码(由于上面设置跟教程一样,所以已经添加SubSonic文件夹时就立刻生成了):

  1. SubSonic3.0模板介绍说明

本文只介绍附件中SubSonic文件夹模板(MsSql),不适用官方发布的模板:

2014年6月份自己学习历程

 

2014年6月16日星期一

  1. 需求分析

cnBlogs技术达人所写,备忘

前言

需求分析文档按正常来说,它不应该由程序员来写的,是由项目经理与客户共同来完成,但是对于国内大多数软件公司(除了少数比较规范的公司专门设置有对应的职位外),很多是需求方口头提出、在WORD写几条要求或提供相关表格文档、提供参考的网站或软件、用相关模型软件简单的做出模型等一种或多种组合方式提出需求,然后由技术部负责人或直接是程序员来编写,当然还有不少情况是根本就没有需求分析这个步骤,需求方直接口头描述需要实现什么功能后,程序员就直接开工......相信大部分朋友正在处于这种水深火热当中或即将进入这种类型的公司。而初学者如果能了解需求文档编写,对以后参与项目的设计与开发将有非常大的帮助。

曾经看到一个园友讲述,他们公司做的外包,用了3个多月做需求分析,花一个月时间编码,一个月时间做测试与修改BUG,然后就交付客户使用了。从中可以看出需求分析在项目开发中的重要性。

当然更多听到的是无尽的吐槽,至于原因在前文已经简单的描述过了。对于需求分析,很多中小型公司都不太在意。我碰到不少拍脑袋的老板和客户,想法很多,变得也快,弄得你无从适合。同他们讲需求,确认功能,真的是一项非常艰巨的任务。而需求没有最终确定,产生的结果就是无限期的需求变动与修改,一个小小的功能可能改上N多次。没有文档就很难评估出你的工作量,通常是加班加点完成功能又没有加班费,而上头还会以为你开发效率低下,一个小功能你就要花费大把时间,浪费金钱。

为什么多数公司会忽略需求分析的重要性,主要原因我觉得有三种,一是需求方也不明白自己要的是什么;二是沟通问题,需求方自认为讲得很清晰了,以为开发方相关人员明白它想要的,而开发方也自认为已经理解了需求方的要求;三是觉得需求很简单,不必要花太多时间浪费,早点开发早点完工,节省开支。

由于篇幅有限,而需求知识点太多,所以本文不会详细描述需求分析的每个步骤,只是简单讲解需求分析的一些常识和本项目相关的需求分析。

需求分析说明

如果严格按照软件工程操作,需求分析阶段有很详细的操作流程,包括需求获取、需求分析、编写需求规格说明、需求验证、知识培训、需求管理、项目管理等等。而对于中小型项目来说,只要求做到前四项基本就足够了。

在处理需求前,首先我们要知道,需求对于需求方来说(不懂技术的),它就像是要实现功能的一份详细说明;一份业务流程;一份表格或文档;甚至可能是一个网站或软件等。他们不太可能与你细说具体要用到什么技术、算法、数据库该存储什么内容、服务器性能、安全等等,而我们如果与他讲太专业的东西,他们大都也会一头雾水,不知道我们在说什么。

 其次,由于大家对各自专业领域的认知有所区别,我们也可能不了解他们专业里的工作流程和具体操作要求。

所以,需求分析最重要的是多沟通、多记录、多论证和多思考

怎么进行需求采集

在同用户的交流过程中收集各种用户的信息与要求,且第一时间将得到的需求整理成文字描述,一一记录下来。在需求采集的过程中,可以要求需求方提供相关的文档、报表、业务流程图等内容给我们参考,然后在这些基础上认真思考在软件上实现的UI大概样子,里面包含什么功能,可能存在什么问题或难点,及时与需求提出者做多次确认,看我们的理解是否是正确的,排除不合理的地方,明确各个业务流程与约束。

那我们这个项目要做什么(实现什么功能)?

第二章已经简单进过,在这里再整理一下:

  1、开发一个有扩展性的框架,以后在这个基础上能实现网站后台、OA、CMR、SCM等各种系统;

  2、要求开发、维护操作要简单,不要那么繁琐(即增加页面、添加修改或删除字段时,操作简单);

  3、有权限管理功能;

  4、实现类似QQ登陆限制的效果,即同一时间一个帐号,只能单独在线,在不同地方登陆时会将前一个踢下线并提醒;也可以设置公众帐号,多人使用同时在线;

  5、用户登陆后使用操作都有详细操作日志记录(即进入什么页面执行了什么操作);

  6、后端管理员只能属于一个部门,但可以有多个职务(角色),有多个职务时也将拥有多个职务的共同权限;

  7、所有页面、按键(工具栏的)操作权限都可以设置,不赋予权限将不能操作;

  8、所有页面访问都需要加密处理,即不能修改页面参数中的属性(比如更新Id值)就可以查看到别人的资料或信息;

......(暂时先实现这些功能吧,其他的以后根据需要再考虑增加)

对于我本人来说,就是通过这个网上很好的教程看来一步步认识一个项目的流程,学会搭建这么一个简单的框架,搞懂框架中各部分的技术知识,从整体上有一个清晰的把握。

占用多长时间

一般对于中小型项目来说,视项目的复杂度与难易度,需求分析大概需要占用3到12个工作日比较合适,当然如果项目涉及到复杂的计算和业务流程,对安全、性能等要求也比较高的,需要分析占用时间也将成倍增长。

编写需求文档

编写需求文档的原则:必须清晰明了描述要求,只描述做什么,而不是怎么做。

对于一些简单的小型项目,前面的需求描述+原型设计可能就已经足够了,有原型的话,在视觉上能更直观。

下面是博主自己写的一个需求文档,仅供参考:

需求确认与变更

编写好需求文档后,要即时与需求方进行确认,将整理出来的问题与难点进行反复讨论确认,然后再次完善需求文档再次确认,直到双方达成共识认可文档。

  当然在整个开发过程中,需求不断变化这是不可避免的,所以在需要分析阶段需要尽可能的将一些可能会产生重大变量的需求提前爆露出来。因为它在开发的不同时间段内提出变更,而对软件产生的影响也是不一样的。小的变更可能会导至开发周期延长,而大的变更,有可能会导至项目推倒重做。

  我们在做需求的时候,要让需求方知道需求变更对项目的影响,以便让他们在考虑需求变更时更加谨慎。当然最好让需求方签属以下内容,有存档为证也可以避免一些不必要的不合理需求出现。

"我同意这份文档表达了目前我们对项目软件需求的了解。进一步的变更可在此基础上通过项目定义的变更过程来进行。我知道变更可能会使我们要重新协商成本、资源和项目时间工期任务等。"(这句话摘自《北京理工大学软件工程实践》汤铭端老师的PPT)

     每次需求变更时,也必须编写对应的变更文档,且让需求方签字确认,这样对计算工期(开发成本)、延长项目进度,以及向上层或需求方反馈我们开发人员工作强度、能力都有明确的凭证。凡事多留个心眼,木有坏处。

我们在开发本框架时,前期虽然设定好框架结构和功能要求,但实际开发中可能会遇到一些不可避免的因素影响,产生一些变化,这时将会认真思考需求变更要求,对于必不可少,必须添加的功能则会直接加入到项目开发中,而对于可有可无的,或对当前项目开发暂时不会产生影响的,将会放到以后开发,一切以开发文档中的设计好的功能要求为准,以做好项目开发周期控制,能按时按质按量完成项目开发。

  1. 后台管理系统功能设计文档

详细参见下面的文档:

在具体的项目中,药根据实际的需求进行功能的设计。

  1. 数据库设计与创建

对于千万级与百万级数据库设计是有所区别的,由于本项目是基于中小型软件开发框架来设计,记录量相对会比较少,所以数据库设计时考虑的角度是:与开发相结合;空间换性能;空间来换开发效率;减少null异常......当然不同的公司与项目要求不同,初学者要学会适应不同的项目开发要求,使用本框架开发时,必须严格按照本章节的要求来设计数据库,不然可能会产生不可控的异常。

数据表设计要求:

  • 数据库表名与字段名应遵守Pascal风格,包含一到多个单词,每一个单词第一个字母大写,其余字母均小写。
  • 如果是关联表,则命名规则为R_表A_表B,如R_ProductInfo_Tag等。
  • 对于视图命名,规则为View_表A,视图由多个表产生,就用下划线连接几个表名,如View_ProductInfo_ProductClass。
  • 存储过程,命名规则为P_表名_存储过程功能名称。如P_ProductInfo_Add;如果该存储过程是很多表共用的,命名为:P_All_存储过程功能名称
  • 数据字段命名,也使用Pascal风格。当字段引用的是其他表字段时,使用表名_其他表字段名称,间用下划线隔开,命名规则:表名_单词。如ProductInfo表与ProductClass表关联的字段:ProductClass_Id,ProductClass_Name等。与外表的主键或相关字段引用时(包括状态值),须同时添加外表所引用主键(或状态值)对应的名称,以方便查询时减少多表关联语句的编写,提高代码执行效率,详细请看《数据字典》中的设计。

数据字典设计要求:

使用附件中的Excel表并按里面的设计格式来设计数据库,如下所示:

2014年6月份自己学习历程

    ExcelToSQLString2.91 for July软件使用简介:

    在设计字段名时,除了上面要求外,还有一些特殊的命名规则要求,这将会影响逻辑层生成的函数,当然有些函数如果名称与下面要求一致,而它却不是你想要的功能定义时,你可以忽略该生成函数,不去调用即可。具体如下:

    主键Id必须命名为"Id",将会生成UpdateValue()函数,用来更新指定Id的记录;

  • 字段名包涵Name这个字串的,会生成GetXxxName()函数,可以直接读取该字段值;
  • 字段名包涵_Id这个字串的,会默认为该字段是外键Id,将会生成DeleteByXxx_Id()函数,用来删除当前表值为指定外键的所有记录;还会生成UpdateValue_For_Xxx_Id()函数,用来更新当前表值为指定外键的记录值;
  • 字段类型为tinyint的字段,会被默认为状态字段,会生成UpdateXxx()函数,用来更新该状态值;
  • 字段名包涵Key这个字串且该字段为string类型的,会生成GetModelByXxxKey()函数,主要功能是通过Key这个字段值来获取当前记录实体;
  • 字段名包涵Count这个字串且该字段为int型的,默认为访问计数字段,会生成UpdateXxxCount()函数,用来执行该字段值累加功能;
  • 字段名包涵Img这个字串且该字段为string类型的,默认为图片路径存储字段,会生成DeleteXxx函数,用来执行图片删除功能;

数据库存储过程创建规范:

在创建存储过程时,头部必须写上功能、创建人、创建日期及修改情况。存储过程主体必须写上清晰的注释说明。

ExcelToSQLString2.91软件的使用方法请参见下面文件:

 

2014年6月17日星期二

  1. 项目实施计划与甘特图

对于很多初学者来说,项目经验不是很足,在实际开发过程中很难把控项目的进度,项目延期和加班加点那是家常便饭了,当然有一部分的原因可能是需求方的变动,而更多的是初学者们制定的开发计划不合理,预计时间不准确有关。

为什么会出现预计的开发时间不准确呢?原因如下:

1)、对项目需求、功能不太了解,不清楚项目涉及的业务逻辑与将要使用的算法,以及功能之间联动产生的影响;

2)、对开发框架或代码不够熟悉,不知道开发一个具体功能要调用到那些模块,需要花多长时间才能完成;

3)、预计的时间只是自己的代码编写完成时间,没有考虑错误修复与自测时间;

4)、在需求方、相关部门或上级领导的压力下(项目完成时间压力),挤压自己的休息时间,将工作以外的加班时间添加到实际开发时间中;

5)、开发经验不足或项目需要的技术不了解,解决问题与Debug占用太多时间;

6)、项目前期规划不好,存在结构性问题,导致代码量大增;

7)、需求方不断变动,未考虑需求变化对项目进度的影响;

8)、UI设计或其他同事开发进度影响;

9)、个人有事请假或其他外因影响,占用了大量开发时间;

        如何预计开发时间和预计项目进度:

首先要做的就是项目各项准备工作,包括了解需求、画出原型,然后设计各界面详细功能,绘制出相关流程图,再了解框架代码和项目中所需要使用的技术细节,做完这些细节一个项目到底要开发多少个界面,使用什么算法,花多时间基本上就心理有数了。

当然这些都是影响预计开发时间的内在因素,除了这些以外,还有很多外在的不可控因素存在,会对项目进度造成更直接的影响。所以初学者在预估开发时间时,还需要注意下面几点:(由于时间关系对于每一点就不再举例说明了,大家自行理解)

1)、在设计项目时,考虑好扩展性,方便需求变动时容易添加新功能;

2)、评估时间必须包括自测时间与Bug修复时间;

3)、对于功能开发,不要随意加入自己的创意,除非必要功能(需要与需求方确认),多余的功能一律不给予实现;(这一点在很多新人身上都会发生,他们大多思路活跃,喜欢在项目中加入自己觉得很不错的小功能或想法,这样的话就会使得项目变得不可控了,多余的功能会占用开发时间,又会使项目产生更多的Bug)

4)、需求变更或增加时,必须通过项目经理或负责人统一规划安排,非必须项一律放到二期以后开发,新增需求必须重新评估开发时间;

5)、开发进度必须严格遵守实施计划的安排,可以提前完成,但不能延期;

6)、与需求方、项目经理、设计师以及部门同事做好充分的沟通工作,有问题主动请教别人,不耻下问;

7)、多请教部门中的前辈和技术部负责人;

除了上面这些外,还有很多其他工作也是需要注意的,如项目更改后原型与文档的同步更新工作(对后期维护与二次开发会产生影响)、测试用例(设计得合理可以提高开发效率,不明白这些的人自测基本上都不会到位的,写出的代码会被测试人员反复的一虐再虐)、项目性能与安全优化(这个就更不用说了,经验不够的就算完成基本代码编写,但后面花费在这上面的时间将更长)、后期的项目部署等等,都会对进度产生一定的影响,这里就不再一一细说了。初学者不可能一开始开发时间就预估得很准,这需要开发经验的不断学习与积累。

相关截图如下所示:

2014年6月份自己学习历程

2014年6月份自己学习历程

2014年6月份自己学习历程

具体内容见下面的数据字典文件:

  1. T4模板在逻辑层中的应用(一)

对于T4模板很多都不太熟悉,它在项目开发中,会帮我们减轻很大的工作量,提升我们的开发效率,减少出错概率。所以学好T4模板的应用,对于开发人员来说是非常重要的。

cnblogs里对于T4模板的介绍与资料已经太多了,所以在这里我就不再详细讲述基础知识了,只是说说T4模板在本框架中的具体应用与实践。

创建逻辑层项目:

2014年6月份自己学习历程

为此类库项目添加之前3个项目的引用,如下所示:

2014年6月份自己学习历程

创建T4模板放置的文件夹,并命名为SubSonic,然后添加Solution.DataAccess项目中SubSonic文件夹下的MultipleOutputHelper.ttinclude、Settings.ttinclude、SQLServer.ttinclude以及项目根目录下的App.config四个文件复制到逻辑层对应的位置里,详见下图:

2014年6月份自己学习历程

SubSonic文件夹里创建Test.tt模板文件,用来练习T4模板的实践:

2014年6月份自己学习历程

Test.TT文本模板内容如下:

2014年6月份自己学习历程

点击保存后,生成的Test.cs文件如下所示:

2014年6月份自己学习历程

编写T4模板实例,练习模板的使用。

练习1

首先添加如下代码:

  • <#@ template language="C#" debug="false" hostspecific="True" #>是T4模板指令,说明使用的语言是C#,不开启debug模式,并将名为 Host 的属性添加到由文本模板生成的类中
  • <#@ output extension=".cs" encoding="utf-8" #>是T4模板的输出指令,限制当前模板生成的文件扩展名为.cs,存储格式为utf-8
  • <#@ include file="SQLServer.ttinclude" #>这是模板的工具类文件,使用 Include 指令在其他文本模板中包含此文件,本行代码主要功能是将SQL操作的工具类(函数)包含到代码
  • using System; 这是输出在cs文件中显示的文本信息,具体功能大家一看就明白了,不再解释
  • namespace <#=Namespace#> 创建命名空间名称,因为SQLServer.ttinclude文件里使用了<#@ include file="Settings.ttinclude" #>,将SubSonic3.0模板的配置信息也同时读取了进来,所以可以直接使用Settings.ttinclude中设置的变量,用它来作为命名空间名称,详见下图:

显然可以看出Namespace是在Setting.ttinclude文件中定义的。也就是说,如果你想在模板中使用一些你想要的变量的话,可以在这些工具类或配置文件中进行定义。

练习2

通过练习一,我们明白了T4模板生成代码的简单原理,那么我们增加些实用的内容来看看模板运行的效果,添加代码如下所示:

2014年6月份自己学习历程

ar tables = LoadTables(); LoadTables()是SQLServer.ttinclude工具类中的函数,功能是获取数据库中所有表和字段(已修改了该文件的代码,可以获取到所有表与视图)

foreach(var tbl in tables)  遍历所有表

<#=tbl.CleanName#>  读取表名称

public class <#=tbl.CleanName#>Table  用表名称+Table 做为类名

点击保存后生成Test.cs文件内容:

    2014年6月份自己学习历程

练习3

通过上面练习,我们可以看到使用很简单的几行代码,就可以非常方便的生成我们想要的代码,减少我们复制粘贴的操作,当然上面生成的东西太简单了,我们想通过本模板生成的类来减少强编码,那么就需要获取所有字段名称出来

新增代码如下:

2014年6月份自己学习历程

foreach(var col in tbl.Columns)  遍历表中所有字段,获取字段结构

<#=Replace(col.Description) #>  本代码中通过col.Description来获取字估注释,Replace函数是将字段注释(说明)里的换行符替换成对应格式:

<#= col.Name #>  获取字段名称

点击保存后生成Test.cs文件内容:

2014年6月份自己学习历程

练习4

用完上面的练习是不是感觉很简单呢。对于数据表比较少的情况下,这种生成是完全没有问题的,但表多了以后都放在一个文件里,在DEBUG调试时就会出问题了,主要原因是代码行数过大,所以我们有另外一种解决办法,就是分文件生成。先上代码(为了更好的理解,会将前面例子中的一些内容删除掉):

2014年6月份自己学习历程

<#@ include file="MultipleOutputHelper.ttinclude"#>  生成多文件工具类

var manager = Manager.Create(Host, GenerationEnvironment);   创建多文件生成实体

foreach(var tbl in tables)  遍历所有表,这个放在using的前面,是因为每个单独文件生成后都需要有using

其他新增内容在代码中都有详细注释了,所以这里不再说明

点击保存后生成Test.cs文件内容:

2014年6月份自己学习历程

2014年6月份自己学习历程

再来个完整的模板代码:

2014年6月份自己学习历程

2014年6月份自己学习历程

点击保存后生成Test.cs文件内容:

2014年6月份自己学习历程

2014年6月18日星期三

  1. T4模板在逻辑层中的应用(二)

上面学习了怎么使用模板类编写T4模板,这次学习一下简单技巧的应用。

        创建一个Test2.tt模板,修改代码如下所示:

2014年6月份自己学习历程

2014年6月份自己学习历程

运行模板,测试看看效果:

2014年6月份自己学习历程2014年6月份自己学习历程

从上面添加函数的方法可以看出,对于常用的函数,都可以在模板中进行添加后,直接生成出来,这样就减少了程序员很大的工作量了。

用上面方法确实可以解决很大部分的问题,但对于一些特殊的函数直接这样写就不行了,比如我们想生成一个删除指定外键Id的函数,由于有的表有外键有的没有,那么就要用上一些简单的判断来处理了:

2014年6月份自己学习历程

同理,我们可以通过判断,生成获取指定名称的字段值和生成删除图片函数等,这些都可以根据你的需要去生成,加入如下代码:

2014年6月份自己学习历程

有时候我们会存在一些特殊的需求,有些表或字段要进行过滤操作,这时我们就可以使用一些简单的过滤判断处理

比如我们对于Manager_Id与Manager_Name这两个字段是不需要生成对应函数的,那么我们就可以加个过滤处理

首先在Settings.ttinclude文件中创建一个字符串数组变量,并赋值:

 

然后在模板中的循环语句中添加判断:

  1. T4模板在逻辑层中的应用(三)

使用Bitconverter自带的静态方法可以非常简单的获取某一类型对应的byte数组或者将byte数组转化为对应类型。如下所示:

2014年6月份自己学习历程

另外一个方法是使用Marshal来实现,Marshal类包含了内存操作的大部分方法,如空间的分配,指针,内存复制,内存读写等。下面是一段实力程序,如下所示:

2014年6月份自己学习历程

    加断点调试并监视相关变量,如下所示:

2014年6月份自己学习历程

仔细分析,不难发现1234567对应16进制数是0x0012D687与对应的byte数组array,由4个字节组成:

array[0]=135(0x87);

array[1]=214(0xD6);

array[2]=18(0x12);

array[3]=0(0x00);

综上所述,整型数转化为的数组一定是低位在前,而低位在前的数组才能直接转换为对应整型数。看来使用Bitconverter方法是最简单的,远比下面的要直接:

以后写程序,不光要写出来,而是要以最简单最直接最不容易犯错的方式写出来。在这里,提醒自己要注意多多练习Linq,消灭不必要的for循环,当然干掉for循环不太可能。

 

2014年6月20日星期五

  1. web层后端登陆功能

对于一个后端管理系统,最重要内容之一的就是登陆页了,无论是安全验证、用户在线记录、相关日志记录、单用户或多用户使用帐号控制等,都是在这个页面进行处理的。

 

有了思路才能写代码,下面是后端管理系统登录验证流程:

2014年6月份自己学习历程

 

2014年6月23日星期一

  1. 只允许一个实例运行

接口程序使用的是控制台应用程序编写,同时运行多个相同的接口程序时会产生不可预料的后果。如产生不必要的异常和程序异常收发等。因此考虑到有必要加入什么使得接口程序只允许一个实例运行,当第N(N>1)个实例运行时检测到已有相同的实例,则该实例5S后退出,这样就避免了多个因为误操作使得多个相同实例相互影响了。

下面是一个简单的Demo:

2014年6月份自己学习历程

运行现象如下:

在ECS接口程序中对应代码如下:

2014年6月份自己学习历程

运行结果如下:

2014年6月份自己学习历程

2014年6月24日星期二

  1. C#使用System.Data.SQLite操作SQLite

新建一个C#控制台应用程序项目,添加System.Data.SQLite.dll库引用,如下所示:

2014年6月份自己学习历程

显而易见,Debug目录下没有test.db数据库文件。SQLite数据库的使用逻辑与MSSQLServer保持一致,添加如下代码:

2014年6月份自己学习历程

运行出现如下异常提示:

2014年6月份自己学习历程

找到SQLite.Interop.dll添加到Debug目录下后运行代码,Debug目录下文件如下所示:

2014年6月份自己学习历程

可见已经成功生成了test.db数据库文件,使用SQLite数据库查看工具SQLiteSpy打开该文件,如下所示:

当然,这种插入数据方式的效率不高,下面进行一个简单的测试。一种是使用上面的方式插入10万条数据,一种是使用事务的方式插入20万条数据,分别输出所需要的时间。第一种插入10万条数据的方法如下:

2014年6月份自己学习历程

运行程序,使用传统方式插入10万条数据所需时间如下:

使用事务的方式插入10万条数据,代码如下所示:

2014年6月份自己学习历程

执行程序,结果如下:

可见,大数据的插入还是使用事务要好很多。

2014年6月25日星期三

  1. 抓取手机在线查询手机归属地的实现方法

页面如下所示:

使用IE浏览器自带的调试工具(按F12即可出现),选择网络后点击捕获按钮,如下所示:

2014年6月份自己学习历程

点击查询按钮,捕获的内容如下所示:

2014年6月份自己学习历程

根据详细信息的内容和请求数据的格式,确定是这个get请求:

复制请求网址,在IE上直接回车,页面如下所示:

可以看到返回的一个json格式的字符串。

 

把上述网址手机号码后面的内容去掉,返回的内容如下所示:

2014年6月份自己学习历程

很明显,返回就是一个XML文件。因此到这里就可以很方便的解析了。为方便起见,下面使用WinForm应用程序测试,关键代码如下所示:

 

2014年6月份自己学习历程

分别输入测试号码15634883123和18902077876如下所示:

2014年6月份自己学习历程2014年6月份自己学习历程

 

2014年6月26日星期四

  1. JQuery Ajax调用页面方法

无参数的方法调用

页面如下所示:

2014年6月份自己学习历程

Js代码如下:

2014年6月份自己学习历程

后台方法如下:

运行,点击验证用户按钮:

2014年6月份自己学习历程

有参数的方法调用

页面同上。

后台代码如下所示:

2014年6月份自己学习历程

注意这里传递参数要保持跟后台方法的形参名称一致。

运行页面,点击验证用户按钮,结果如下所示:

把上面js代码的参数名称改变,使其与定义是不一致:

结果如下所示:

返回数组方法

页面代码在上面的基础上添加了<ul id='list'></ul>.

后台方法如下所示:

2014年6月份自己学习历程

Js代码如下:

2014年6月份自己学习历程

2014年6月份自己学习历程

2014年6月27日星期五

  1. 将控制台应用程序改为windows服务

windows服务概述:

Microsoft Windows 服务能够创建在它们自己的 Windows 会话中可长时间运行的可执行应用程序。这些服务可以在计算机启动时自动启动,可以暂停和重新启动而且不显示任何用户界面。这使服务非常适合在服务器上使用,或任何时候,为了不影响在同一台计算机上工作的其他用户,需要长时间运行功能时使用。还可以在不同于登录用户的特定用户帐户或默认计算机帐户的安全上下文中运行服务。本文就向大家介绍如何运用Visual C#来一步一步创建一个文件监视的Windows服务程序,然后介绍如何安装、测试和调试该Windows服务程序。

新建一个windows服务项目,如下所示:

2014年6月份自己学习历程

程序会自动生成一个继承ServiceBase的类,在这里将类重命名为NCSInterface。在该类的设计器中右键选择添加安装程序,如下所示:

2014年6月份自己学习历程

程序将会生成一个ProjectInstaller.cs的类,该类继承与System.Configuration.Install.Installer,如下所示:

2014年6月份自己学习历程

 

上面的InitializeComponent();方法在该类的另一部分(声明为partial),如下所示:

2014年6月份自己学习历程

上面加注释的部分修改对应服务的名字和描述以及服务账户、用户名、密码等。这里的服务名称必须要和继承了ServicBase的服务类中的服务类中一致,如下所示:

2014年6月份自己学习历程

2014年6月份自己学习历程

上面的程序注意引用服务程序文件的目录是AppDomain.CurrentDomain.BaseDirectory,而不是Environment.CurrentDirectory

除此之外,还有一个Propram.cs文件除此之外还有一个Program.cs文件,此文件作用是使得一个Windows服务程序能够正常运行,我们需要像创建一般应用程序那样为它创建一个程序的入口点。在Windows服务程序中,我们也是在Main()函数中完成这个操作的。首先我们在Main()函数中创建一个Windows服务的实例,该实例应该是ServiceBase类的某个子类的对象,然后我们调用由基类ServiceBase类定义的一个Run()方法。然而Run()方法并不就开始了Windows服务程序,我们必须通过前面提到的服务控制管理器调用特定的控制功能来完成Windows服务程序的启动,也就是要等到该对象的OnStart()方法被调用时服务才真正开始运行。如果你想在一个Windows服务程序中同时启动多个服务,那么只要在Main()函数中定义多个ServiceBae类的子类的实例对象就可以了,方法就是创建一个ServiceBase类的数组对象,使得其中的每个对象对应于某个我们已预先定义好的服务。

将当前的windows服务项目framework设置为4.0、X86平台,编译该项目生成对应的文件,如下所示:

如果直接运行编译生成的NCSInterfaceService.exe文件,提示无法运行,如下所示:

2014年6月份自己学习历程

将NCS接口程序对应文件全部拷贝到该服务的debug文件夹下,如下所示:

2014年6月份自己学习历程

当前,其中有很多的临时文件,这个等调试完毕删掉即可。

Windows服务程序必须使用InstallUtil.exe工具安装或者卸载服务,其格式如下所示:

把4.0的InstalUtil.exe复制到debug目录下,以管理员运行cmd并切换到该目录,敲入如下命令,并执行:

2014年6月份自己学习历程

在服务中启动该服务,如下所示:

查看对应的NCSInterface.exe是否在进程中:

说明服务正常启动了接口程序。同样,键入如下命令并回车可以卸载该服务:

2014年6月份自己学习历程

至此,基本内容完成。但是需要写BAT脚本,使得能够比较方便的安装和卸载服务。

编写一个命令行文件来安装本服务,如下所示:

2014年6月份自己学习历程

运行该脚本文件,安装服务失败,提示如下:

2014年6月份自己学习历程

找不到文件并且framework版本不一致,因此InstallUtil.exe换为4.0目录下的,NCSInterfaceService.exe文件前加.\,如下所示:

2014年6月份自己学习历程

上面的BAT文件不正确,继续查资料。。。

 

2014年6月30日星期一

  1. 使用NPOI读取Excel文件

要测试的Excel文件为MenuInfo.xlsx,内容如下所示:

2014年6月份自己学习历程

下载NPOI2.0官方库,并添加引用(一共5个dll文件),如下所示:

2014年6月份自己学习历程

根据官方文档,读取上面的Excel文件,代码如下:

2014年6月份自己学习历程

2014年6月份自己学习历程 2014年6月份自己学习历程

这里需要注意,2003的Excel文件后缀为xls,而2007版的Excel文件后缀为xlsx,两者的文件内部格式不一致,使用NPOI处理时,分别对应HSSFWorkbook和XSSFWorkbook

加入断点运行程序,Excel文件返回的DataTable如下所示:

2014年6月份自己学习历程

2014年6月份自己学习历程

具体代码参见如下文件:

 

2014年7月1日星期二

  1. 使用NPOI读取Excel文件

要测试的Excel文件为MenuInf

 

2014年7月2日星期三

  1. 编写自己使用的Log日志记录类

原来的日志类每天写入同一个文件(这个文件名为当天的日期),但是时间长了之后,文件会比较多,如下所示:

2014年6月份自己学习历程

上面的就有38个日志文件,因此要改为一个能够限定日志文件数量的日志类。刚开始打算使用日志的修改时间,但是可能文件随时被修改而文件名相对来说不会改变,因而要采用使用文件名排序把较早的日志删除掉,只保留最新的日志。

要需要解决的问题如下:

  • 获取对应目录的所有后缀为.log的文件
  • 判断某一文件是否在此目录存在
  • 如何读取道德.log文件按照名称排序
  • 如何删除某一文件

File.Exist(path)方法可以判断指定文件是否存在,File.Delete(path)方法可以删除指定文件。获取对应目录的所有后缀为.log的文件方法如下:

对获取的文件进行排序,需要自己定义一个实现了IComparer的排序类,规则是按照名称降序排序,定义的排序类如下:

2014年6月份自己学习历程

详细的测试程序代码如下:

程序运行结果:

2014年6月份自己学习历程

PS:参考别人写的,自己只是备档学习。如原作者介意,我会删除相关内容[email protected]

 

你可能感兴趣的:(学习)