在 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 下载页面下载最新构建版本。
