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: // /file/test.htm （或 http: // /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: // /file等可以进入到我们的独立服务器而http: // / 也可以由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</vp>来表示的：

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</vp> 接口表示：

interface jsonObject supports mapM{string Key, jsonValue Value} ... end interface jsonObject

该接口含有一些便利的谓词/属性（这些上面都没有说到），不过从本质上来说jsonObject</vp>不过就是从Key（名称）到JSON值之间的映射.

创建 RPC 服务时，必须实现一个支持rpcService</vp>接口的对象：

interface testService supports rpcService end interface testService

class testService : testService end class testService

在该实现中，需要继承处理JSON解析、编写等的jsonRpcServiceSupport</vp>：

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</vp> 用于注册将参数放在数组中的过程
 * setProc_name</vp> 用于注册在对象中使用命名参数的过程
 * setListener_array</vp> 用于注册将参数放在数组中的通知监听器
 * setListener_name </vp> 用于注册在对象中使用命名参数的通知监听器

在上面的例子中，有两个过程“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</vp>过程首先创建一个jsonObject</vp>用于Result</vp>. 任意 JSON 值都可以当成结果，不过 hello</vp> 设计为返回带一个"token"</vp>成员的JSON对象.

接下来它由ArgMap</vp>获取参数，要的是一个名为"username"</vp>的串参数.

<vp>hello</vp> 使用 <vp>Username</vp> 的值来说明异常处理与成功.

<vp>"error"</vp>一段演示如何引发“服务器错误”，它是用于RPC服务的特别设计的错误消息. “userError”和“definiteUserError”情况说明的是该过程调用时除其它异常外可能发生的异常.

要注意，客户端代码要编程处理相应的错误. 当然服务器一侧也可以在任意其它程序中处理指定的异常，也可以在适当地方记录这些错误.

错误不是 <vp>Username</vp> 的情况，映射 "token" 串给 <vp>Result</vp>.

独立的 HTTP 服务器
如同上面 <vp>jsonRpcService_httpApi</vp> 程序演示的，<vp>testService</vp>（支持 <vp>rpcService</vp>）可以用于独立的 HTTP 服务器，不需要多少代码，而且为适合特殊需要而做的代码调整也很简单. 但不管怎么样，也还是相当复杂的.

该服务器基于微软能与IIS很好配合的HTTP Server API. 其实，IIS好像是构建在该 HTTP Server API 的顶层（或起码它们两者都是构建在共同基础之上的）.

HTTP Server api 与以下实体打交道：
 * 创建器/构造器的进程
 * Server会话
 * URL组
 * Request队列
 * Worker进程

创建器/构造器进程创建服务器会话，由其定义URL组及请求队列. worker进程与请求队列相关联，处理队列接收到的请求.

在示例程序中创建器/构造器是仅有的worker进程，更复杂的情形超出了本教材的范围.

服务器会话是URL组及请求队列的所有者/持有者，它们才是我们需要实际考虑的.

URL组是由URL模型集定义的，其形式为：
 * http: // : /<relativeURI>
 * https: // : /<relativeURI>

一个请求到达计算机时，一种匹配机制会按照活动的URL组中URL模型集对其进行匹配，结果要不是匹配不上就是覆盖该URL.

每个URL组分配一个请求队列，与某个URL组匹配的请求就放入其相应的队列. 那些没有分配请求队列的URL组是非活动的，不参加匹配.

Worker进程从请求队列中取出请求并进行处理. 一个请求队列可以与若干个URL组相联系，而每个URL组又可以有若干URL模型. 因此，worker进程由某个请求队列摘出的请求可以会匹配与该队列联系的某个URL组的某个URL模型.

在示例服务器中，我们创建了两个请求队列，每个都只有一个URL组与之联系，并且每个URL组也只有一个URL模型. 事实上：
 * <vp>reqQueue_rpc</vp> 接收来自 http: //localhost:5555/jsonrpc/ 的请求，而
 * <vp>reqQueue_file</vp> 接收来自http: //localhost:5555/file/ '的请求.

<vp>reqQueue_rpc</vp> is a <vp>requestQueue_rpc</vp> handles RPC requests by feeding them to our <vp>testService</vp>.

<vp>reqQueue_file</vp>是一个<vp>requestQueue_file</vp>，它用 <vp>fileMapper</vp>映射该请求到一个文件名及相应的<vp>ContentType</vp>，<vp>requestQueue_file</vp>接下来就会处理该文件的传输.

服务器配置：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插件
如同 <vp>jsonRpcService_isApi</vp> 程序所演示的那样，<vp>testService</vp>（支持<vp>rpcService</vp>)也可以ISAPI dll的形式来应用. 该DLL可以由IIS使用来运行RPC请求.

<vp>jsonRpcService_isApi</vp> 不处理文件请求，更自然的办法是把它留给IIS本身. 该DLL代码其实不复杂，关键是IIS的配置.

IIS配置有专门教材，参见IIS configuration.

GUI程序
在webRPC示例目录中还有一个<vp>jsonRpcService_httpGUI</vp>程序，它演示了图形用户界面程序如何用作独立Web RPC服务. 要注意，并不提倡实际的服务程序具有GUI. 这只是说明，当需要其它程序与GUI程序交互时GUI很有用，比如当用户登录时. 它也可以用作一种快速借用已有GUI代码用于Web服务的方法（比如在创建实际服务前的试验）.

多线程
不管web服务如何运行（独立的带或不带GUI、或是ISAPI），需要注意的是HTTP请求运行于并发的若干线程. 所以，保证请求处理的线程安全是很重要的. <vp>requestQueue_rpc::synchronizer</vp>属性可用于同步RPC的执行，这个属性可以由一个谓词进行设置接收<vp>runnable</vp>，而<vp>runnable</vp>时才实际执行RPC调用. 可运行的程序可以是，比如在一个监控程序（monitor）下运行，以此来保证这个时间只有一个程序在运行；或者是放入一个队列并从中顺序执行；再或者是如 <vp>jsonRpcService_httpGUI</vp> 那样发布给GUI队列交由GUI线程执行.

Visual Prolog 客户端
本教材及所提及的示例主要讨论Visual Prolog服务器一侧内容，而使用web浏览器作为客户端. 不过，有时Visual Prolog程序也需要作为客户端访问WEB服务. 示例 <vp>jsonRpcClient</vp> 演示了如何做这样的事情. 其方法与使用JavaScript时很相似：


 * 创建一个带有方法名及参数的 <vp>jsonRpcRequest</vp> 对象
 * 用 <vp>asString</vp> 对其进行JSON串化
 * 用 XMLHttpRequest 对象（<vp>xmlHTTP</vp> 或 <vp>xmlHTTP60</vp>的一个实例）进行投递
 * JSON 解析响应文本（用<vp>jsonObject::fromString</vp>）
 * 检查 "error" 成员

在示例程序中，有一个 <vp>delayCall</vp>，,它是为了使GUI得到更新. 如果没有它，GUI的更新就要一直等到RPC完成. 不过这个调用与RPC本身是无关的.

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