mormot 1.18 第十八章 使用rest/json的客户端/服务器
json是一种被多种语言和众多领先公司接受的标准。正如我们在json章节中所解释的,它是标准化的,紧凑且解析速度快,同时当加入非关键性空格时,也易于人类阅读。这些事实使其成为数据交换最受欢迎的格式之一。
json支持六种数据类型:
json类型 | 描述 |
---|---|
数字 | javascript中的双精度浮点数格式,通常取决于具体实现。没有特定的整数类型 |
字符串 | 双引号括起来的unicode,带有反斜杠转义 |
布尔值 | true 或 false |
数组 | 一个有序的值序列,以逗号分隔并括在方括号中;这些值不需要是同一类型 |
对象 | 一个无序的"键值对"集合,使用':'字符分隔键(key)和值(value),这些键值对被逗号分隔并包含在大括号中;其中的键必须是字符串且应该各不相同。 |
null | 空值/未定义的值 |
结构性字符包括大括号{}、中括号[]、冒号:和逗号,。当你查看示例时,你会发现像电话号码这样的复杂格式可以简单地视为字符串处理。
比如:
{ "firstname": "john", "lastname": "smith", "age": 25, "address": { "streetaddress": "21 2nd street", "city": "new york", "state": "ny", "postalcode": 10021 }, "phonenumbers": [{ "type": "home", "number": "212 555-1234" }, { "type": "fax", "number": "646 555-4567" } ] }
json的默认编码是utf8,与sqlite3和ewb相同。这允许存储和传输完整的unicode字符集在客户端和服务器之间。
当我们需要存储或传输二进制blobs时,使用base64编码。
下表描述了pascal变量是如何转换的:
pascal类型 | 备注 |
---|---|
boolean | 序列化为json布尔值 |
byte, word, integer, cardinal, int64, single, double, currency | 序列化为json数字 |
string, rawutf8, synunicode, widestring | 序列化为json字符串 |
datetime, ttimelog | 序列化为json文本,编码为iso 8601 |
rawbytestring | 序列化为json的null或base64编码的json字符串 |
rawjson | 存储为未序列化的原始json内容(例如任何值、对象或数组) |
tguid | guid序列化为json文本 |
嵌套记录 | 序列化为json对象,标识为record ... end; 或 { ... },包含其嵌套定义 |
嵌套注册记录 | 序列化为与定义的回调相对应的json |
记录的动态数组 | 序列化为json数组,标识为array of ... 或 [ ... ] |
简单类型的动态数组 | 序列化为json数组,例如标识为array of integer |
静态数组 | 序列化为json数组,通过增强的rtti处理,尚未通过文本定义处理 |
variant | 序列化为json,完全支持tdocvariant自定义变体类型 |
18.1 rest
rest(表征状态转移)是一种从客户端向服务器请求/传输信息,并从服务器返回信息到客户端的策略。
- 一切都是资源
- 每个资源都通过一个唯一标识符(uri中的i)来标识
- 使用简单且统一的接口
- 通过表征进行通信
- 每个请求都是无状态的
使用rest时,你会用到uri(唯一资源标识符)来标识资源。
例如:
客户 | 数据uri |
---|---|
获取名为“dupont”的客户的详细信息 | http://www.mysite.com/customer/dupont |
获取名为“smith”的客户的详细信息 | http://www.mysite.com/customer/smith |
获取名为“dupont”的客户所下的订单 | http://www.mysite.com/customer/dupont/orders |
获取名为“smith”的客户所下的订单 | http://www.mysite.com/customer/smith/orders |
实际上,叫“dupont”和“smith”的人很多,所以通常人们会使用像客户编号、十六进制值或guid这样的唯一id。
crud操作的接口包括post、get、put和delete。
http方法 | 操作 |
---|---|
get | 列出集合中的一个或多个成员 |
put | 更新集合中的一个成员 |
post | 在集合中创建一个新条目 |
delete | 删除集合中的一个成员 |
表征是我们描述对象的方式,通常使用json或xml来完成。
xml表征:
<customer> <id>1234</id> <name>dupond</name> <address>tree street</address> </customer>
相同的json表征:
{"customer": {"name":"dupond", "address":"tree street"}}
使用xml或json通过post添加记录将返回刚刚创建的id。
无状态意味着每个请求都是独立的。这意味着服务器甚至可以在请求之间重新启动,或者负载均衡器可以将请求转发到不同的实际主机。服务器不维护任何形式的状态表,如会话变量等。
结果是更精简、更高效、更可扩展的服务器或集群。
18.2 restful mormot
当一个系统实现了非常接近纯粹rest的东西时,该系统被称为restful系统。
mormot可以使用http/https,但它也适用于使用windows安全模型的命名管道。
它还可以使用另一种不需要使用昂贵的证书及其缓慢握手的加密形式——这是谷歌也曾描述过的一个话题。这对于封闭的mormot客户端(多平台)和服务器系统非常有用。但你仍然可以访问基于标准的更慢、更常见的版本。
mormot允许以下非标准功能来加速对记录的访问。
它们是可选的。
- lock,用于锁定集合中的一个成员;
- unlock,用于解锁集合中的一个成员;
- begin,用于启动事务;
- end,用于提交事务;
- abort,用于回滚事务。
blobs通过单独的事务处理,使用的uri类似于modelroot/tablename/tableid/blobfieldname。
这样做的优点是能够使用高效的二进制传输,而将blobs存储在json中大约需要两倍的空间/数据/时间。
18.3 传输方式的选择
传输方式是指客户端和服务器之间通信使用的方法。目前,mormot支持四种传输方式:
- 进程内通信——在同一进程内部进行最高速度的访问。
- windows消息——在同一台机器上的进程之间进行非常快速的通信。与进程内通信相比,这种方式略有开销。非常适合少数客户端,但不具备扩展性。
- 命名管道——在同一台或不同的windows机器上的两个进程之间进行快速通信。适合工作组使用,但扩展性不好。
- http/https——在internet或私有内网上的任意两台计算机之间进行相当快速的通信。使用标准技术。在win32中可以扩展到50,000多个,在64位模型中可以扩展到更多。
比较这些方法:
进程内(in-process) | windows消息(messages) | 命名管道(named pipe) | http/https | |
---|---|---|---|---|
实现单元(unit) | mormot.pas | mormot.pas | mormot.pas | |
速度(speed) | 最快(fastest) | 极快(extremely fast) | 很快(very fast) | 快(fast) |
扩展性(scaling) | 最佳(受限于ram)(best limited by ram) | 较差(例如10)(poor eg. 10) | 较差(poor) | 非常好(very good) |
托管(hosting) | 单一进程(one process) | 单一计算机(one computer) | 局域网/互联网(lan/internet) | 内网(intranet) |
协议(protocol) | 方法调用(method call) | wm_copydata | \\pc\mormot | https://pc/... |
数据(data) | json | json | json | json |
服务(service) | 是(yes) | 否(no) | 是(yes) | 是(yes) |
客户端(client) | tsqlrestclientdb | tsql | ||
服务器(server) | tsqlrestserverdb | tsqlrestserverdb | tsqlrestserverdb | tsqlrestserverdb |
exportserver | exportservermessage | exportserver namedpipe | tsqlhttp |
windows消息版本通常不太实用,因为包括服务器进程在内的所有应用程序必须处于图形用户界面(gui)模式,并且都在同一台机器上运行。
命名管道在通信中曾很受欢迎,但从windows vista开始,对命名管道的局域网访问默认是关闭的,因此您必须手动启用它。
http/https是一个很好的通用解决方案。它被普遍接受,具有良好的扩展性,并且已经过每台服务器超过50,000个连接的测试。通信速度仍然很快。
请注意,甚至可以同时使用多种协议。例如,使用进程内通信来完成某些任务,同时也提供局域网或http版本的服务。
18.4 http/https 传输
以下是如何使用http初始化数据库的方法。
model := createsamplemodel; dbserver := tsqlrestserverdb.create( model,changefileext( paramstr(0), '.db3'),true); dbserver.createmissingtables; httpserver := tsqlhttpserver.create('8080', [dbserver],'+', http_default_mode);
通常,您还可以使用以下行允许跨站点的ajax查询:
httpserver.accesscontrolalloworigin := '*'; // 允许跨站点ajax查询
您通常会使用以下设置进行域名重定向:
httpserver.domainhostredirect('project.com','root'); // 'root' 是当前的 model.root httpserver.domainhostredirect('blog.project.com','root/blog'); // mvc 应用程序
包含三种类型的服务器,但幸运的是,mormot 可以简单地从最快的选项故障转移到最兼容的选项,只要是被允许的:
- thttpapiserver - 使用 windows 的高速内部内核模式 http.sys 驱动程序,并且如果您选择,它还可以实现用于 tls/ssl 安全通信的 https。这应该是您的首选。
- thttpserver 是基于套接字或 websockets 的实现。
18.5 https
要启用 https,您必须在 tsqlhttpserver.create() 构造函数的 ahttpserversecurity 参数中设置 secssl。
您还需要证书。您可以使用本地证书颁发机构或商业机构之一。这些按照 windows iis 的通常方式安装。
mormot 网站包含当前所有操作系统的该过程的最新说明。
18.6 aes加密 - https的替代方案
aes是一种加密选项,不需要分发ssl的pki对。
首先,积极的一面是:
- 它使用了标准的sha-256和aes-256/ctr算法,这些算法都很好。这意味着没有共享密码的人阅读数据流将无法拦截查询或结果,也无法制造虚假的查询或结果。
- 它的速度也比https快。
不利的一面是:
- aes在客户端和服务器之间使用共享密钥,如果密钥被泄露,所有数据都可以通过适当的嗅探技术被读取。
- pki的作用不仅仅是防止中间人查看数据,它还保证你正在连接的是你请求的服务器。如果有人知道这个秘密,他可能会制造一些极有可能会成功的攻击。
- 虽然该算法是标准的,但它的应用却不是。非mormot编译的客户端无法使用此技术。
如果你能忍受这些限制,aes是一个很好的选择。
在服务器上,可以通过secsynshaaes启用它。
compressshaaessetkey('gudmw324dajklaf(*\&' ); myserver := tsqlhttpserver.create( '888',[database],'+',usehttpapi,32,secsynshaaes);
在客户端上,你只需将compression属性设置为hcsynshaaes。
compressshaaessetkey('gudmw324dajklaf') myclient.compression := [hcsynshaaes];
你还应该考虑压缩,因为它可以通过减少可用于收集信息的信息内容(例如数据包大小)来提高安全性。
18.7 压缩
压缩有几个目的:
- 减少慢速链接上的数据传输时间;
- 降低昂贵链接(企业上行链路、手机数据费用)上的数据成本;
- 混淆数据...尤其是与上述加密结合使用时。mormot支持两种加密方法。
- deflate - 一种基于zip的标准压缩技术。它的缺点是它有点cpu密集型,但也能达到最好的效果;
- synlz - 一种专有的方法,对服务器cpu负载更轻。它只适用于mormot编码的客户端,不适用于ewb或其他人的客户端。
你可以单独启用一种压缩或另一种压缩。我建议同时启用两者,这样当synlz可用时,mormot将使用synlz进行压缩,否则默认使用deflate,并完全基于标准。
myclient.compression := [hcsynlz,hcdeflate];
18.8 示例
以下是一个在8080端口上实现http协议的极简数据库服务器。
program cssimpleserver; {$apptype console} uses sysutils, syncommons, mormot, mormotsqlite3, synsqlite3static, // 引入必要的单元 mormothttpserver, csclass in 'csclass.pas'; var model: tsqlmodel; // sql模型变量 db: tsqlrestserverdb; // 数据库变量 server: tsqlhttpserver; // http服务器变量 s: string; // 字符串变量 procedure start; begin model := createsamplemodel; // 创建样本模型 db := tsqlrestserverdb.create(model, 'd:\cstest.db3', true); // 创建数据库连接 db.createmissingtables; // 创建缺失的表 server := tsqlhttpserver.create('8080', [db], '+', http_default_mode); // 在8080端口上创建http服务器 server.accesscontrolalloworigin := '*'; // 设置跨域资源共享策略,允许任何来源的请求 end; procedure stop; begin server.free; // 释放http服务器资源 db.free; // 释放数据库资源 model.free; // 释放模型资源 end; begin try start; // 启动服务器 writeln('按"enter回车"键退出'); // 提示用户按回车键退出 readln(s); // 读取用户输入,等待用户按下回车键 except on e: exception do // 异常处理 writeln(e.classname, ': ', e.message); // 输出异常类型和异常信息 end; stop; // 停止服务器并释放资源 end.
注释说明:
tsqlmodel
:表示数据库模型的类,定义了数据库的结构和关系。tsqlrestserverdb
:表示基于rest的数据库服务器类,用于处理数据库的crud操作。tsqlhttpserver
:表示http服务器类,用于监听http请求并返回响应。createsamplemodel
:是一个自定义函数,用于创建一个示例的数据库模型。db.createmissingtables
:调用此方法会创建在数据库中缺失的表,这些表是基于model
定义的。server.accesscontrolalloworigin
:设置http响应头access-control-allow-origin
,用于控制哪些源可以访问该资源,'*'
表示允许任何源访问。readln(s)
:用于等待用户输入,直到用户按下回车键,程序才会继续执行。这里主要是为了让程序持续运行,直到用户主动停止。stop
过程中释放资源的顺序很重要,通常先释放依赖其他资源的对象(如server
),再释放被依赖的资源(如db
和model
)。
它使用了一个小的tsqlrecord派生类:
program cssimpleserver; {$apptype console} uses system.sysutils, syncommons, mormot, mormotsqlite3, synsqlite3static, // 引入必要的单元 mormothttpserver, csclass in 'csclass.pas'; var model: tsqlmodel; // sql模型变量 db: tsqlrestserverdb; // 数据库变量 server: tsqlhttpserver; // http服务器变量 s: string; // 字符串变量 procedure start; begin model := createsamplemodel; // 创建样本模型 db := tsqlrestserverdb.create(model, 'd:\cstest.db3', true); // 创建数据库连接 db.createmissingtables; // 创建缺失的表 server := tsqlhttpserver.create('8080', [db], '+', http_default_mode); // 在8080端口上创建http服务器 server.accesscontrolalloworigin := '*'; // 设置跨域资源共享策略,允许任何来源的请求 end; procedure stop; begin server.free; // 释放http服务器资源 db.free; // 释放数据库资源 model.free; // 释放模型资源 end; begin try start; // 启动服务器 writeln('press return to exit'); // 提示用户按回车键退出 readln(s); // 读取用户输入,等待用户按下回车键 except on e: exception do // 异常处理 writeln(e.classname, ': ', e.message); // 输出异常类型和异常信息 end; stop; // 停止服务器并释放资源 end.
注释说明:
tsqlmodel
:表示数据库模型的类,定义了数据库的结构和关系。tsqlrestserverdb
:表示基于rest的数据库服务器类,用于处理数据库的crud操作。tsqlhttpserver
:表示http服务器类,用于监听http请求并返回响应。createsamplemodel
:是一个自定义函数,用于创建一个示例的数据库模型。db.createmissingtables
:调用此方法会创建在数据库中缺失的表,这些表是基于model
定义的。server.accesscontrolalloworigin
:设置http响应头access-control-allow-origin
,用于控制哪些源可以访问该资源,'*'
表示允许任何源访问。readln(s)
:用于等待用户输入,直到用户按下回车键,程序才会继续执行。这里主要是为了让程序持续运行,直到用户主动停止。stop
过程中释放资源的顺序很重要,通常先释放依赖其他资源的对象(如server
),再释放被依赖的资源(如db
和model
)。
这个客户端也很简单。
{$apptype console} uses system.sysutils, syncommons, mormot, mormothttpclient, // 引入必要的单元 csclass in 'csclass.pas'; var model: tsqlmodel; // 定义sql模型变量 db: tsqlhttpclient; // 定义http客户端数据库连接变量 s: string; // 定义字符串变量 procedure start; var server: ansistring; // 定义服务器地址变量 begin if paramcount = 0 then // 如果没有传入命令行参数 server := 'localhost' // 则默认服务器为本地 else server := ansistring(paramstr(1)); // 否则取第一个命令行参数为服务器地址 model := createsamplemodel; // 创建样本模型 db := tsqlhttpclient.create(server, '8080', model); // 创建http客户端数据库连接 db.setuser('user', 'synopse'); // 设置数据库用户 end; procedure stop; begin db.free; // 释放数据库连接 model.free; // 释放模型 end; // 读取指定用户的记录 procedure readone(user: string); var rec: tsqlsamplerecord; res: string; begin try rec := tsqlsamplerecord.create(db, 'name = ?', [stringtoutf8(user)]); if rec.id = 0 then res := 'not found' else res := utf8tostring(rec.question); writeln('question for ', user, ' is ', res); finally rec.free; end; end; // 写入指定用户的记录 procedure writeone(user, question: string); var rec: tsqlsamplerecord; begin try rec := tsqlsamplerecord.create; rec.name := stringtoutf8(user); rec.question := stringtoutf8(question); if db.add(rec, true) = 0 then writeln('error: adding message to db!') else writeln('message added.') finally rec.free; end; end; var user: string; // 定义用户名字符串变量 begin try start; // 启动程序 user := 'erick'; // 设置用户名为erick readone(user); // 读取用户记录 writeone(user, 'happy day'); // 写入用户记录 readone(user); // 再次读取用户记录 except on e: exception do // 异常处理 writeln(e.classname, ': ', e.message); // 输出异常类名和异常信息 end; stop; // 停止程序 end.
注释说明:
tsqlmodel
是一个代表数据库模型的类,用于定义数据库的结构。tsqlhttpclient
是一个用于与http服务器通信的客户端类,它实现了通过http协议与服务器进行数据交换的功能。createsamplemodel
是一个创建样本模型的函数,它返回一个tsqlmodel
实例,该实例包含了数据库中所有表的结构信息。readone
函数用于从数据库中读取指定用户的记录,并输出相关信息。writeone
函数用于向数据库中写入指定用户的记录,并输出操作结果。- 在
try...except
块中调用start
、readone
、writeone
和stop
等函数,以确保在程序运行过程中出现异常时能够正常处理并释放资源。同时,通过输出异常信息来帮助定位问题所在。 - 程序首先通过
start
函数初始化数据库连接和模型,然后通过调用readone
和writeone
函数来读取和写入用户记录,最后在stop
函数中释放资源并结束程序运行。
注意:本文由hieroly翻译于2024年04月26日
发表评论