[Delphi] YxdIOCP 之 MVC 简介
最近为 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 对象,用于直接输出返回数据。
- TIocpHttpConnection、TIocpClientContext:自动注入当前连接对象(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: 允许哪些请求头可以跨域