Web Services (Web服务)
(以下内容,译自Category:Tutorials中的Web Services。更多内容可以参看Category:Chinese。)
本教程描述如何创建Web应用程序,它的前端是运行在用户浏览器中的HTML/JavaScript,而后端是用Visual Prolog编写的。
介绍的重点是Visual Prolog的后端,它实现JSON-RPC Web服务。所包含的HTML/JavaScript代码主要是为了使例子自身完整,说明前端代码如何与Visual Prolog服务互动。
教程描述了同样的服务如何才能:
- 作为独立的HTTP/HTTPS服务器运行
- 作为IIS(Microsoft Information Services微软信息服务)的ISAPI扩展嵌入式地运行
注意,web服务需要使用Visual Prolog的 商业版。 还要注意,本教程中完全没有考虑SOAP。
使用的例子是商业版中的 webRPC。 (IDE: Help ->Install Examples...)。
概述
本节描述应用程序的整体概貌及工作方式。实际发生的事情是相当复杂的,不过好在很多复杂的东西是自动处理的。了解过程的概貌及其复杂性,就比较容易理解要做些什么及为什么这样做。
总的来说,作为例子的应用程序其工作流程是这样的:用户(在web浏览器中)浏览网页 http://<server>/file/test.htm (或 http://<server>/file/calendar.htm),服务器返回相应的文件给浏览器。浏览器对文件的上下文进行评估后,可能进一步请求级联的css、js文件和图片等。此外,HTML文件包含的嵌入式JavaScript代码也会对服务器产生远端过程调用 (RPC),服务器执行相应的过程并将结果返回给浏览器。最后,嵌入式的JavaScript用返回的数据更新浏览器的HTML内容。
客户机与服务器之间的通信由HTTP (或 HTTPS)协议完成。因此在服务器一侧需要一个HTTP服务器,它可以返回文件并实现远端过程调用。在 jsonRpcService_httpApi 中的程序运行该服务作为一个独立的HTT服务器,它可以处理文件请求,实现远端过程调用。jsonRpcService_isApi中的程序是用ISAPI插件来实现远端过程调用的,通过对IIS的配置来处理文件请求并对远端过程调用使用ISAPI。
独立的HTTP服务器通过微软的HTTP Server API来实现,它可以共享带有IIS的端口及URL,这样一来,http://<server>/file等可以进入到我们的独立服务器而http://<server>/<something>也可以由IIS得到处理。
JSON 和 JSON-RPC
远端过程调用是按照JSON-RPC 2.0 Specification规程实现的,这实际上要求数据按JSON (JavaScript Object Notation) 格式编码。
JSON语法是JavaScript的一个子集:在对有效JSON文本进行求值时它会成为一个相应的JavaScript对象。然而,还是应该总是使用JSON解析程序而不是对该文本求值:因为文本更可能是“代码”而不是“数据”,所以对文本求值有安全上的风险。
不需要对JSON格式有详细的了解,因为在客户端一侧使用的是JavaScript而在服务器一侧使用的是JSON的对象/域表示方式,(对JSON格式的)解析和编写都可以交给标准软件去做。
执行JSON-RPC调用时,客户端发送一个JSON请求对象,服务器用一个JSON响应对象做响应,除非请求是一个notification(通知),此时不需要响应。
请求对象有四个成员:
- jsonrpc: 一个说明JSON-RPC协议版本的串,必须就是 "2.0"。
- method: 一个包含被调用方法名称的串。
- params: 该方法的参数(如果方法不带参数就省略)。
- by-position: 一个JSON数组,包含有按服务器要求顺序排列的值。
- by-name: 一个带有成员名称的JSON对象,成员名称要严格与方法所期望的参数相匹配,包括大小写都要一致。
- id: 一个由客户端创建的标识该请求的串或数。如果是通知则这个域就省掉了。
响应对象有三个成员:
- jsonrpc 一个说明JSON-RPC协议版本的串,必须就是 "2.0"。
- id 由客户端发布的值。
第三个成员是下列之一:
- result (意味着该调用成功了)程序调用的结果
- error (意味着该调用失败了) 一个描述所发生错误的对象
客户端一侧
有很多其它的方法来写客户端一侧的代码,通常会使用一些标准的JavaScript库程序包,这些包可以异步处理错误及执行调用。 calendar.htm 使用了这样的库代码,不过这些内容超出了本教材的范围。
服务代码
在Visual Prolog中JSON是由域json::jsonValue来表示的:
domains jsonValue = n; % null f; % false t; % true r(real Number); s(string String); a(jsonValue* Array); o(jsonObject Object).
JSON值只有7类:null、false、true、数、串、数组、对象。
数组是一个JSON值的序列,用Visual Prolog的表来表示。
JSON对象由Visual Prolog的 jsonObject 接口表示:
interface jsonObject supports mapM{string Key, jsonValue Value} ... end interface jsonObject
该接口含有一些便利的谓词/属性(这些上面都没有说到),不过从本质上来说jsonObject不过就是从Key(名称)到JSON值之间的映射。
创建 RPC 服务时,必须实现一个支持rpcService接口的对象:
interface testService supports rpcService end interface testService class testService : testService end class testService
在该实现中,需要继承处理JSON解析、编写等的jsonRpcServiceSupport:
implement testService inherits jsonRpcServiceSupport open core, json ... end implement testService
还需要在构造器中注册你的过程:
clauses new() :- setProc_name("hello", hello), setProc_name("calendar", calendar).
过程有四类,相应于不同的注册谓词:There are four kinds of procedures, with corresponding registration predicates:
- setProc_array 用于注册将参数放在数组中的过程
- setProc_name 用于注册在对象中使用命名参数的过程
- setListener_array 用于注册将参数放在数组中的通知监听器
- setListener_name 用于注册在对象中使用命名参数的通知监听器
在上面的例子中,有两个过程“hello”和“calendar”,都是使用命名参数。 hello 的代码可以是这样的:
constants testService_error : jsonRpcError::serverErrorCode = jsonRpcError::serverError_last+1. % 从last开始增加,last是用于未指明的服务器错误 predicates hello : jsonProc_name. clauses hello(_Context, ArgMap) = o(Result) :- Result = jsonObject::new(), Username = ArgMap:get_string("username"), if "error" = Username then raise_serviceError(testService_error, "'error' is an invalid user name") elseif "userError" = Username then exception::raise_user("'%Name%' is an invalid user name", [namedValue("Name", string(Username))]) elseif "definiteUserError" = Username then exception::raise_definiteUser("'%Name%' is an invalid user name", [namedValue("Name", string(Username))]) else Result:set_string("token", string::concat("Hello ", Username)) end if.
hello过程首先创建一个jsonObject用于Result。任意 JSON 值都可以当成结果,不过 hello 设计为返回带一个"token"成员的JSON对象。
接下来它由ArgMap获取参数,要的是一个名为"username"的串参数。
hello 使用 Username 的值来说明异常处理与成功。
"error"一段演示如何引发“服务器错误”,它是用于RPC服务的特别设计的错误消息。“userError”和“definiteUserError”情况说明的是该过程调用时除其它异常外可能发生的异常。
要注意,客户端代码要编程处理相应的错误。当然服务器一侧也可以在任意其它程序中处理指定的异常,也可以在适当地方记录这些错误。
错误不是 Username 的情况,映射 "token" 串给 Result。
独立的 HTTP 服务器
如同上面 jsonRpcService_httpApi 程序演示的,testService(支持 rpcService)可以用于独立的 HTTP 服务器,不需要多少代码,而且为适合特殊需要而做的代码调整也很简单。但不管怎么样,也还是相当复杂的。
该服务器基于微软能与IIS很好配合的HTTP Server API。其实,IIS好像是构建在该 HTTP Server API 的顶层(或起码它们两者都是构建在共同基础之上的)。
HTTP Server api 与以下实体打交道:
- 创建器/构造器的进程
- Server会话
- URL组
- Request队列
- Worker进程
创建器/构造器进程创建服务器会话,由其定义URL组及请求队列。worker进程与请求队列相关联,处理队列接收到的请求。
在示例程序中创建器/构造器是仅有的worker进程,更复杂的情形超出了本教材的范围。
服务器会话是URL组及请求队列的所有者/持有者,它们才是我们需要实际考虑的。
URL组是由URL模型集定义的,其形式为:
- http://<host>:<port>/<relativeURI>
- https://<host>:<port>/<relativeURI>
一个请求到达计算机时,一种匹配机制会按照活动的URL组中URL模型集对其进行匹配,结果要不是匹配不上就是覆盖该URL。
每个URL组分配一个请求队列,与某个URL组匹配的请求就放入其相应的队列。那些没有分配请求队列的URL组是非活动的,不参加匹配。
Worker进程从请求队列中取出请求并进行处理。一个请求队列可以与若干个URL组相联系,而每个URL组又可以有若干URL模型。因此,worker进程由某个请求队列摘出的请求可以会匹配与该队列联系的某个URL组的某个URL模型。
在示例服务器中,我们创建了两个请求队列,每个都只有一个URL组与之联系,并且每个URL组也只有一个URL模型。事实上:
- reqQueue_rpc 接收来自 http://localhost:5555/jsonrpc/<something>的请求,而
- reqQueue_file 接收来自http://localhost:5555/file/<something>'的请求。
reqQueue_rpc is a requestQueue_rpc handles RPC requests by feeding them to our testService.
reqQueue_file是一个requestQueue_file,它用 fileMapper映射该请求到一个文件名及相应的ContentType,requestQueue_file接下来就会处理该文件的传输。
服务器配置:netsh
上面的示例运行很平稳,因为它使用的是宿主机5555端口。然而一般情况下运行应用程序会要求http的使用80端口、https的使用443端口,这时就会出现一些安全性问题。
如果仅只是更换端口号并使用公共宿主机名,在给URL组添加URL模型时服务器程序将会得到一个 'Access is denied'(拒绝访问)的异常。
如果"as Administrator"来运行服务器则不会产生访问违例。但这绝对是不值得提倡的,因为这样一来它也有了做某些有害事情的权力。
要使程序运行于普通模式,必须要对运行服务器程序的用户保留上面讨论的URL模型。
使用netsh.exe (net shell)程序可以完成这个工作。 program.这将会需要处理http URL acl。详细内容参见 Netsh commands for HTTP.
通过Visual Prolog来进行URL保留也是可以的,但为了防止冲突及其它一些问题,最好还是使用标准netsh程序。
注意示例是个控制台程序。在大规模应用情况下设计一个通用窗口服务会更好些。Visual Prolog有工程模板用于创建服务,但这超出了本教材的范围。
IISAPI插件
如同 jsonRpcService_isApi 程序所演示的那样,testService(支持rpcService)也可以ISAPI dll的形式来应用。该DLL可以由IIS使用来运行RPC请求。
jsonRpcService_isApi 不处理文件请求,更自然的办法是把它留给IIS本身。该DLL代码其实不复杂,关键是IIS的配置。
IIS配置有专门教材,参见IIS configuration。
GUI程序
在webRPC示例目录中还有一个jsonRpcService_httpGUI程序,它演示了图形用户界面程序如何用作独立Web RPC服务。要注意,并不提倡实际的服务程序具有GUI。这只是说明,当需要其它程序与GUI程序交互时GUI很有用,比如当用户登录时。它也可以用作一种快速借用已有GUI代码用于Web服务的方法(比如在创建实际服务前的试验)。
多线程
不管web服务如何运行(独立的带或不带GUI、或是ISAPI),需要注意的是HTTP请求运行于并发的若干线程。所以,保证请求处理的线程安全是很重要的。requestQueue_rpc::synchronizer属性可用于同步RPC的执行,这个属性可以由一个谓词进行设置接收runnable,而runnable时才实际执行RPC调用。可运行的程序可以是,比如在一个监控程序(monitor)下运行,以此来保证这个时间只有一个程序在运行;或者是放入一个队列并从中顺序执行;再或者是如 jsonRpcService_httpGUI 那样发布给GUI队列交由GUI线程执行。
Visual Prolog 客户端
本教材及所提及的示例主要讨论Visual Prolog服务器一侧内容,而使用web浏览器作为客户端。不过,有时Visual Prolog程序也需要作为客户端访问WEB服务。示例 jsonRpcClient 演示了如何做这样的事情。其方法与使用JavaScript时很相似:
- 创建一个带有方法名及参数的 jsonRpcRequest 对象
- 用 asString 对其进行JSON串化
- 用 XMLHttpRequest 对象(xmlHTTP 或 xmlHTTP60的一个实例)进行投递
- JSON 解析响应文本(用jsonObject::fromString)
- 检查 "error" 成员
在示例程序中,有一个 delayCall,,它是为了使GUI得到更新。如果没有它,GUI的更新就要一直等到RPC完成。不过这个调用与RPC本身是无关的。
Skype HTTP/HTTPS 冲突
Skype (potentially) conflicts with HTTP/HTTPS servers, because Skype (by default) reserves port 80 (HTTP) and 443 (HTTPS).
As a result you cannot run an HTTP/HTTPS server when Skype runs (in default mode).
The solution is to turn off port 80/443 in Skype.
See this video: Handling port conflicts with Skype on Windows
HTTPS
HTTPS是使用安全套接的HTTP协议。IIS和独立HTTP服务器都可以使用HTTPS。要使用HTTPS,服务器计算机必须要有一个证书。可以自己创制证书,但自制的证书是不可信的,因而会引发安全提示。自制证书用于试验与开发是够用了,但在实际使用时需要获取一个可信的证书。
简单说,可信证书必须从可信提供者那里获取。这种信任基于以下三件事:
- 提供者是自身可信的
- 提供者(通过正常购买链)对证书持有者真实身份具有认知
- 提供者可以在证书被滥用时撤消证书
注意,HTTPS缺省时使用443端口(而HTTP使用80端口),需要时必须在使用"http"的地方写明 "https"(例如https://+:443/mypath)
对证书的进一步讨论超出了本教材的范围。
参考
- JSON
- JSON-RPC 2.0 Specification
- Microsoft's HTTP Server API