TsgcHTTPRESTServer:全新的 REST 服务器组件 | eSeGeCe 博客

TsgcHTTPRESTServer:全新的 REST 服务器组件

· 组件
sgcWebSockets TsgcHTTPRESTServer 组件

TsgcHTTPServer 之上构建 REST API 一直都是可行的,但有几件事每次都必须手工编写:CORS 预检响应、供负载均衡器使用的健康检查端点、供监控系统抓取的指标端点,以及某种把不同客户的数据区分开的办法。TsgcHTTPRESTServer 是一个新组件,它把所有这些都开箱内置了。

它直接继承自 TsgcHTTPServer,因此你已经熟悉的一切依然适用:同样的 Port、同样的 SSLOptions、同样的 Authentication、同样的 OnCommandGet 处理器。TsgcHTTPServer 仍然是一个普通的 HTTP 服务器,不会发生任何变化。新增的功能都在派生组件里,而且每一项都需要显式启用。

快速上手

该组件位于 sgcHTTP_REST_Server 单元中,以 TsgcHTTPRESTServer 的名字注册在 SGC REST 组件面板页上。把它放到窗体上并设置 Active,你就得到了一个可用的 HTTP 服务器;真正有意思的部分是请求处理器。

uses
  sgcHTTP_REST_Server;

var
  oServer: TsgcHTTPRESTServer;
begin
  oServer := TsgcHTTPRESTServer.Create(nil);
  oServer.Port := 8080;
  oServer.OnCommandGet := OnServerCommandGet;
  oServer.Active := True;
end;

处理器使用标准的 Indy 请求与响应对象,因此一个 JSON 响应只需三次赋值:

procedure TForm1.OnServerCommandGet(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
  if ARequestInfo.Document = '/api/status' then
  begin
    AResponseInfo.ResponseNo := 200;
    AResponseInfo.ContentType := 'application/json';
    AResponseInfo.ContentText := '{"status":"running"}';
  end
  else
  begin
    AResponseInfo.ResponseNo := 404;
    AResponseInfo.ContentType := 'application/json';
    AResponseInfo.ContentText := '{"error":"not found"}';
  end;
end;

OnCommandGet 接收 GET 和 POST。PUT、PATCH、DELETE 之类的动词则进入 OnCommandOther,它的签名完全相同,因此一个支持全部动词的 REST 资源通常写成一个分发例程,由这两个事件共同调用。

procedure TForm1.OnServerCommandOther(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
  if ARequestInfo.Command = 'DELETE' then
  begin
    AResponseInfo.ResponseNo := 204;
    AResponseInfo.ContentText := '';
  end;
end;

CORS,默认关闭是有意为之

浏览器从另一个源调用你的 API 时需要 Access-Control-* 响应头,而且在发出真正的请求之前,它还需要先得到 OPTIONS 预检的应答。CORSOptions 把这两件事都处理好了。

唯一值得强调的一点是:Enabled 默认为 False,而且除非你确实需要,否则就应该保持这样。一台服务器如果在升级之后悄悄开始以 Access-Control-Allow-Origin: * 作答,那就是一次安全倒退,所以 CORS 严格采用显式启用的方式。

oServer.CORSOptions.Enabled := True;
oServer.CORSOptions.AllowOrigins := 'https://app.example.com';
oServer.CORSOptions.AllowMethods := 'GET, POST, PUT, DELETE, OPTIONS';
oServer.CORSOptions.AllowHeaders := 'Content-Type, Authorization';

这样配置之后,预检会自动以 204 和这三个响应头作答,而每一个普通响应也都会被加上这些头。你不需要在处理器里写 OPTIONS 分支。

只要 API 带有认证,就应优先使用明确的源而不是 *。通配符源与凭据的组合本来就会被浏览器拒绝,而把你实际服务的源逐个列出来是更安全的默认做法。

无需自己编写的健康检查与指标

把一个 TsgcHTTPServerStats 组件挂到 ServerStats 属性上,服务器就能提供两个运维端点。在你逐个启用它们之前,两者都处于禁用状态。

oStats := TsgcHTTPServerStats.Create(nil);
oStats.Endpoints.Health.Enabled := True;
oStats.Endpoints.Metrics.Enabled := True;
oServer.ServerStats := oStats;

/health 返回一小段 JSON 文档,适合用作负载均衡器的探针:

{"status":"ok","uptime":3600,"connections":12,"requests":48120,
 "responses":{"1xx":0,"2xx":47800,"3xx":10,"4xx":300,"5xx":10},
 "latency":{"min":0,"avg":4,"max":180}}

/metrics 返回 Prometheus 文本展示格式,因此中间不需要任何适配器就能被抓取:

# HELP sgc_server_requests_total Total requests served
# TYPE sgc_server_requests_total counter
sgc_server_requests_total 48120
# HELP sgc_server_request_duration_ms_avg Average request duration
# TYPE sgc_server_request_duration_ms_avg gauge
sgc_server_request_duration_ms_avg 4

如果 /metrics/health 与你自己的路由冲突,这两个路径都是可以配置的:

oStats.Endpoints.Metrics.Path := '/internal/metrics';
oStats.Endpoints.Health.Path := '/internal/health';

这些端点是在认证关卡之后提供的,因此它们会继承服务器已经强制执行的任何认证。除非服务器本身是公开的,否则它们绝不会对外公开。这一点在两个方向上都值得了解:它让这些端点默认保持私有,同时也意味着当服务器要求凭据时,监控抓取程序也必须提供凭据。

在提供 API 的同时提供静态内容

DocumentRoot 继承自 TsgcHTTPServer 并且照常工作,因此单台服务器可以同时托管一个小型前端和它所调用的 API。凡是你的处理器没有应答的请求,都会落到文档根目录。

oServer.DocumentRoot := 'C:\www\app';
oServer.HTTPCompression.Enabled := True;
oServer.HTTPCompression.MinSize := 1024;

TLS

相对于基础服务器没有任何变化。像往常一样设置 SSL 并填好 SSLOptions

oServer.SSL := True;
oServer.SSLOptions.Port := 443;
oServer.Port := 443;

接下来是什么

该组件还发布了 Tenancy 属性和只读的 Tenant 属性,它们把一台服务器变成面向多客户的服务器;而 Authentication 选项接受一个用户存储组件,于是你不必再把凭据保存在 TStringList 里。这些内容会在第二篇文章中介绍,第三篇则展示如何在这一切之前放上一份 OpenAPI 契约,让路由、校验和文档全部来自同一个文件。

TsgcHTTPRESTServer 现已推出。请从 sgcWebSockets 下载页面下载最新构建版本。