mORMot 1.18 第18章 使用REST/JSON的客戶端/伺服器

海利鸟發表於2024-05-03

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回車"鍵退出');  // 提示使用者按Enter鍵退出
    readln(s);             // 讀取使用者輸入,等待使用者按下Enter鍵
  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):用於等待使用者輸入,直到使用者按下Enter鍵,程式才會繼續執行。這裡主要是為了讓程式持續執行,直到使用者主動停止。
  • Stop過程中釋放資源的順序很重要,通常先釋放依賴其他資源的物件(如 Server),再釋放被依賴的資源(如 DBModel)。

它使用了一個小的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');  // 提示使用者按Enter鍵退出
    readln(s);             // 讀取使用者輸入,等待使用者按下Enter鍵
  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):用於等待使用者輸入,直到使用者按下Enter鍵,程式才會繼續執行。這裡主要是為了讓程式持續執行,直到使用者主動停止。
  • Stop過程中釋放資源的順序很重要,通常先釋放依賴其他資源的物件(如 Server),再釋放被依賴的資源(如 DBModel)。

這個客戶端也很簡單。

{$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塊中呼叫 StartreadonewriteoneStop等函式,以確保在程式執行過程中出現異常時能夠正常處理並釋放資源。同時,透過輸出異常資訊來幫助定位問題所在。
  • 程式首先透過 Start函式初始化資料庫連線和模型,然後透過呼叫 readonewriteone函式來讀取和寫入使用者記錄,最後在 Stop函式中釋放資源並結束程式執行。

注意:本文由hieroly翻譯於2024年04月26日

相關文章