[Delphi] YxdIOCP 之 MVC 简介

原文地址: http://www.cnblogs.com/yangyxd/articles/5993764.html

YxdIocp:  https://github.com/yangyxd/YxdIOCP

 

最近为 YxdIocp 开源库增加了这个轻量级的 MVC 支持。其实说是 MVC ,但由于现在还没有配套的视图模版引擎,所以在V上面很虚弱。不过本着做数据服务为主的思想,现在已经基本够用了。下面来简单介绍一下它吧。

要使用 MVC 功能非常简单, 引用 iocp.Http.MVC 单元,或者在窗体上放入一个 TIocpHttpMvcServer 组件即可。然后就是编写业务代码 (可参考开源库中 demo\IcopHttpSvrMVC 中的代码)。iocp.Http.MVC 引入了多个属性标注,它是 MVC 的灵魂,要用好 MVC, 必须了解这些属性标注的功能和意义。MVC引擎会使用扫描器根据这些标注自动扫描注入,响应客户请求。

在此之前,我们需要注意一点,在 Delphi 中, 属性标注定义的名称尾部如果是 “Attribute”,  在使用时可以直接省略后面的 “Attribute” ,比如定义了“ServiceAttribute”标注,使用时可以只输入 “[Service]” 即可。下面讲到的属性标注,都是省略掉了 “Attribute” 。

 

一、属性标注说明

[Service]

业务层标识性标注,为指定类添加此标注后,此类才会被MVC引擎识别,从而具备业务请求处理能力。

uses
  iocp.Http.MVC, iocp.Http, SysUtils, Classes;

type
  [Service]
  THelloMvc = class(TObject)
  public
  end;

 

在上面的示例代码中,由于加入了 [Service] 标识,会被 MVC 扫描器发现该类,并自动创建一个单例用于响应业务请求。

 

[Controller]

控制层标注。增加此标注后,指定类才具备处理业务请求的能力。此标注和 [Service] 功能一样,只是方便在一些情况下区分业务类别。

uses
  iocp.Http.MVC, iocp.Http, SysUtils, Classes;

type
  [Controller]
  THelloMvc = class(TObject)
  public
  end;

 

[Autowired]

自动装配标注。为一个类中的变量增加此标注后,MVC 引擎会在实例化此类时,为这个成员变量自动注入适当的值。(目前由于暂无IOC实现,只能自动注入TIocpWebSocketServer、Ser ver)

type
  [Controller]
  THelloMvc = class(TObject)
  protected
    [Autowired]
    FServer: TIocpHttpServer;
  public
  end;

 

程序运行后,会自动将 FServer 初始化为 HttpMVC.Server 。

[RequestMapping]

请求地址映射标注。此标注可用于类级别,也可用于类中的函数或方法上。

此标注有如下几个属性:

  • Value:  指定请求的实际地址。
  • Method: 指定请求的method类型, 它是 TIocpHttpMethod 类型的枚举值,可以是 GET、POST、PUT、DELETE 等。
  • Consumes: 指定处理请求的提交内容类型(Content-Type),例如application/json, text/html。
  • Produces: 指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回。
  • Params: 指定request中必须包含某些参数值时,才让该方法处理。
  • Headers: 指定request中必须包含某些指定的header值,才能让该方法处理请求。

注意: 在类级别使用此标注时, Params 属性无效。类级别主要用于设置URL的基址,以及一些默认值。比如在类级别指定 Method 为 Post,那么类中的函数或方法映射时,如果未指定 Method, 那么默认就是 Post。另外,在函数或方法被触发时,以下类型的参数会被自动注入:

  • TIocpHttpRequest: 自动注入为当前Request对象。
  • TIocpHttpResponse: 自动注入会当前Response对象。
  • TIocpHttpServer: 自动注入会当前Server对象。
  • TIocpWebSocketServer: 启用 WebSocket 时,自动注入当前 Server 对象。
  • TIocpHttpWriter: 自动注入 Response.GetOutWriter 对象,用于直接输出返回数据。
  • TIocpHttpConnectionTIocpClientContext:自动注入当前连接对象(Request.Connection)。
  • TMemoryStream 或基于 TStream: 自动注入当前请求的数据流对象(Request.Data)。

另外,如果参数名是 “RequestData”, 且为字符串类型时,自动注入 Request.DataString 。

 

示例

unit Unit2;

interface

uses
  iocp.Http.MVC, iocp.Http, SysUtils, Classes;

type
  [Controller]
  [RequestMapping('/hello')]
  THelloMvc = class(TObject)
  public
    // 映射地址:/hello/view1
    // 响应所有类型的请求(Get, Post, Put 等等)
    [RequestMapping('/view1')]
    function View1(Request: TIocpHttpRequest): string;

    // 映射地址:/hello/view2
    // 响应 Get 请求
    [RequestMapping('/view2', http_GET)]
    function View2(Request: TIocpHttpRequest): string;

    // 映射地址:/hello/view3/aaa?uid=123
    // 响应 Get 请求, 并且必须是包含参数uid,值为123时才响应
    // 如果参数中不包括uid,或者uid的值不为123,会返回405错误
    [RequestMapping('/view3/aaa', http_GET, 'uid=123')]
    function View3(Request: TIocpHttpRequest): string;

    // 映射地址:/hello/view4?uid=123
    // 必须是 Get 请求, Content-Type必须包含application/json,
    // Accept必须是"*.*"或者包含“application/json” ,
    // 必须包含参数uid=123
    // 如果不符合条件会返回405错误
    [RequestMapping('/view4', http_GET, 'application/json', 'application/json', 'uid=123')]
    function View4(Request: TIocpHttpRequest): string;
  end;

implementation

{ THelloMvc }

function THelloMvc.View1(Request: TIocpHttpRequest): string;
begin
  Result := 'httpPostTest.html';
end;

function THelloMvc.View2(Request: TIocpHttpRequest): string;
begin
  Result := 'httpPostTest.html';
end;

function THelloMvc.View3(Request: TIocpHttpRequest): string;
begin
  Result := 'httpPostTest.html';
end;

function THelloMvc.View4(Request: TIocpHttpRequest): string;
begin
  Result := 'httpPostTest.html';
end;

initialization
  THelloMvc.RegToMVC;

end.

 

如果服务器Host是: http://127.0.0.1:8080,

那么,

 

URL: http://127.0.0.1:8080/view1

HTTP: 不限(GET, POST, PUT…)

触发: THelloMvc.View1

 

URL: http://127.0.0.1:8080/view2

HTTP: GET

触发: THelloMvc.View2

 

URL: http://127.0.0.1:8080/view3/aaa?uid=123

HTTP: GET

触发: THelloMvc.View3

 

URL: http://127.0.0.1:8080/view3/aaa?uid=789

HTTP: GET

触发: 无,返回405错误

 

URL: http://127.0.0.1:8080/view3

HTTP: GET

触发: 无,返回404错误

 

URL: http://127.0.0.1:8080/view4?uid=123

HTTP: GET

触发: 无,返回405错误

原因: 没有在Http Header 的”Content-Type”属性中添加 “application/JSON”,需要使用类似下面的请求头才可以正常访问:

GET /hello/view4?uid=123 HTTP/1.1
Accept: */*
User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:12.0) Gecko/20100101 Firefox/12.0
Content-Type: application/JSON; charset=GB2312
Accept-Encoding: gzip, deflate
Content-Length: 0
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Connection: Keep-Alive

使用上面的请求头,会触发 THelloMvc.View4 。

 

[Download]

标识请求返回页面采用下载方式。加入此标注后,会在Http响应头中加入文件下载标识,浏览会以文件下载的方式读取数据,一般会提示用户保存文件。

type
  [Controller]
  THelloMvc = class(TObject)
  public
    [Download]
    function FileDownload(Request: TIocpHttpRequest): string;
  end;

[WebSocket]

WebSocket 请求处理标注。当类中的一个函数或方法要用来处理 WebSocket 请求时,需要添加此标注。此标注不能与 [RequestMapping] 混用。

此标注有如下几个属性:

  • Data: 指定仅当Data为指定内容的文本消息时才响应。
    // WebSocket 请求处理,直接返回字符串内容
    [WebSocket]
    function HelloWebSocket(): string;

    // WebSocket 请求处理, 只有接收到文本信息且内容是 'hello' 时才响应
    [WebSocket('hello')]
    procedure HelloWebSocket2(Response: TIocpWebSocketResponse);

[PathVariable]

这是一个参数级的属性标注,用来获得请求url中的动态参数,并绑定到处理函数中指定参数上。如果参数类型与实际传入的内容不符,会产生 500 错误。

此标注有如下几个属性:

  • Name: 当方法参数名称和需要绑定的uri template中变量名称不一致时, 用于指定uri template的名称。如果一致,可以省略。

URL中的动态参数,需要以 “{}” 包围起来。

    [RequestMapping('/view/{uid}/{uname}', http_GET)]
    procedure ViewTest1(
      [PathVariable('uid')] UID: Integer;
      [PathVariable('uname')] const UName: string;
      Response: TIocpHttpWriter);

 

示例中,{uid}、{uname} 都是url级的动态参数。通过 PathVariable 标注将它们分别绑定到了处理函数 ViewTest1 的参数 UID 和 UName 上。在触发 ViewTest1 时,UID 的值就是 {uid} , UName 就是 {uname} 。

 

[RequestParam]

这是一个参数级的属性标注。用于将请求参数区数据映射到功能处理方法的参数上。如果实际URL中参数不存在时,会将参数置为空(如数字型会是0,字符串会是空串)。如果参数类型与实际传入的内容不符,会产生 500 错误。

此标注有如下几个属性:

  • Name: 参数名称。
    // 返回一个网页名称, 以下载方式
    // 处理 URL: /demo/view4?uid=123456
    [RequestMapping('/view4', http_GET)]
    function ViewTest4([RequestParam('uid')] UID: Integer): string;

示例中,当 url 是 http://host/demo/view4?uid=123456 时, 触发 ViewTest4 时,参数 UID 的值会是 123456。

 

[RequestBody]

这是一个函数(方法)级的属性标注。它的作用如下:

1. 读取Request请求的body部分数据,使用系统默认配置的Converter(转换器)进行解析,然后把相应的数据绑定到要返回的对象上;

2. 再把Converter返回的对象数据绑定到controller中方法的参数上。

示例:

type
  TUserData = record
    UID: Integer;
    Age: Integer;
    Name: string;
    Nick: string;
  end;
 
    ......

   // 提交用户数据
    // 处理 URL: /demo/person/profile/reguser
    [RequestMapping('/person/profile/reguser')]
    function RegUser([RequestBody] Data: TUserData): string;

在触发 RegUser 函数时, 转换器会将请求的数据注入 Data 中。在编写转换器时,GET请求一般需要单独处理。

转换器示例:

设置 HttpMvc.OnDeSerializeData 事件,在事件中进行处理:

// 反序列化处理
function TForm1.DoDeSerializeData(Sender: TObject; const Value: string;
  const Dest: TValue; IsGet: Boolean): Boolean;
var
  Json: JSONObject;
  S: AnsiString;
begin
  if not IsGet then begin
    Json := JSONObject.ParseObject(Value, False);
    try
      if Assigned(Json) then
        TYxdSerialize.ReadValue(Json, Dest);
    finally
      FreeAndNil(Json);
    end;
  end else begin
    // Get 请求单独处理
    Json := JSONObject.Create;
    try
      S := AnsiString(Value);
      HttpMvc.Server.DecodeParam(PAnsiChar(S), Length(S), DoReadedItem, Json);
      TYxdSerialize.ReadValue(Json, Dest);
    finally
      FreeAndNil(Json);
    end;
  end;
  Result := True;
end;

在上面的反序列化操作中, Dest 实际上就是标注了 RequestBody 的响应函数中的正要注入的参数,在示例中就是 Data: TUserData。

如果有这样的一个Post请求:

POST /demo/person/profile/reguser HTTP/1.1
Accept: */*
User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:12.0) Gecko/20100101 Firefox/12.0
Content-Type: text/html; charset=GB2312
Accept-Encoding: gzip, deflate
Content-Length: 59
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Connection: Keep-Alive

{"UID":666,"Age":30,"Name":"yangyxd","Nick":"\u55B5\u55B5"}

在触发 RegUser 时, 参数 Data 的值会是:

Name    Value
Data    (666, 30, ‘yangyxd’, ‘喵喵’)

 

[ResponseBody]

这是一个函数(方法)级的属性标注。它的作用如下:

将Controller的方法返回的对象,通过适当的Converter的Adapter转换对象, 将内容转换为指定格式后,写入到Response对象的body数据区。

    // 根据UID查询用户信息。
    // 处理 URL: /demo/person/profile/123456
    [RequestMapping('/person/profile/{id}', http_GET)]
    [ResponseBody]
    function Porfile([PathVariable('id')] UID: Integer): TPerson;

在触发 Porfile 函数时, 返回值 TPerson 会被转换器转换序列化为指定格式,然后写入 Response 对象, 返回给客户端。

设置 HttpMvc.OnSerializeData 事件,在事件中进行处理:

// 序列化处理
function TForm1.DoSerializeData(Sender: TObject; const Value: TValue): string;
var
  Json: JSONObject;
begin
  Json := JSONObject.Create;
  try
    TYxdSerialize.WriteValue(Json, '', Value);
  finally
    Result := Json.ToString();
    Json.Free;
  end;
end;

如果在 Porfile 中的返回内容是:

function TMvcDemo.Porfile(UID: Integer): TPerson;
begin
  Result.UID := UID;
  Result.Name := 'Admin';
  Result.Status := 100;
end;

通过使用上面的转换器, 在触发 Porfile 函数后,浏览器收到的数据会是:

{"UID":123456,"Name":"Admin","Status":100}

 

二、映射函数返回值

普通WEB请求:

无返回值时, 会返回客户端 200 状态。

返回字符串时, 会返回一个  Prefix + 返回值 + Suffix 的文件。如果文件不存在,则返回 404 错误。

返回整数时, 认为是错误代码。比如返回 404, 则浏览器会收到 404 错误。

返回对象(Class) 或记录(Record)时,如果标注了ResponseBody,会使用转换器转换后输出给客户端。如果没有标注 ResponseBody, 则直接返回 200 状态。如果返回的是对象,会自动释放对象。

 

WebSocket 请求:

无返回值时, 服务器不作任何响应。

返回字符串时,会直接将字符串发送给客户端。

返回数据时,会将数据转为字符串后发送给客户端。

返回对象(Class) 或记录(Record)时,如果标注了ResponseBody,会使用转换器转换后输出给客户端。如果没有标注 ResponseBody, 服务器不作任何响应。

三、 MVC 服务器配置

在窗口上放置 TIocpHttpMvcServer 组件时, 通过组件属性面板进行设置。

不使用 TIocpHttpMvcServer 组件时, 引用 iocp.Http.Mvc , 会自动开启 MVC 服务。此时会自动加载配置文件 “http_mvc_setting.xml“。

配置文件应当放于服务程序相同目录中,文件名为 “http_mvc_setting.xml”

配置文件示例:

<?xml version="1.0" encoding="UTF-8"?>
<xml>
    <ListenPort>8081</ListenPort>
    <Active>true</Active>
    <Charset>UTF-8</Charset>
    <UseWebSocket>false</UseWebSocket>
    <ContentLanguage/>
    <WebPath>.\Web\</WebPath>
    <GzipFileTypes>.htm;.html;.css;.js;.txt;.xml;.csv;.ics;.sgml;.c;.h;.pas;.cpp;.java;</GzipFileTypes>
    <AutoDecodePostParams>true</AutoDecodePostParams>
    <UploadMaxDataSize>2097152</UploadMaxDataSize>
    <AccessControlAllow>
        <Enabled>false</Enabled>
        <Origin>*</Origin>
        <Methods>POST, GET, OPTIONS</Methods>
        <Headers>X-Requested-With, Content-Type</Headers>
    </AccessControlAllow>
</xml>

 

配置文件说明:

ListenPort: 服务器监听端口

Active: 是否在程序运行后自动开始服务

Charset: 默认字符集

UseWebSocket: 是否使用WebSocket服务

ContentLanguage: 默认内容语言

WebPath: WEB服务文件根目录

Prefix: 返回视图的前缀名称,默认为空

Suffix: 返回视图的后缀名称,默认为空

UriCaseSensitive: URI是否大小写敏感

BindAddr: 服务器端口绑定地址,默认为“0.0.0.0”

GzipFileTypes: 自动使用gzip压缩数据的文件类型

AutoDecodePostParams: 自动解析Post方式传入的参数

UploadMaxDataSize: 上传文件的最大大小(字节)。

AccessControlAllow: WEB请求跨域控制选项。

Enabled: 是否启用跨域控制

Origin: 允许哪些url可以跨域请求到本域

Methods: 允许的请求方法

Headers: 允许哪些请求头可以跨域

 

分享到: