用 R 开发一个 RESTful 服务器

近日偶然发现 R 语言有一个 **plumber** 包,可以用来开发类似于 express.js 的服务器。目前来看功能还比较简单,应该和 Django 或者 Egg.js 等企业级框架是没法比的。但是可堪一用,在发布算法服务方面又多了一种选择。

这里就以发布一个简单的 RESTful 的 Basic GWR 算法服务为例进行演示。

首先我们编写一个 `app.R` 用来编写应用的启动命令

```R
library(plumber)
app <- pr("routes.R")
pr_run(app, port = 6000)
```

其中的 `pr()` 函数通过读取 R 源码生成路由,当然也可以编写函数,但是 R 由于暂不支持像 Python 或 JavaScript 那样从一个脚本文件中导入符号(除非用 `source()` 命令执行),所以如果想分开编写的话,还是要传入文件名。

然后编写 `routes.R` 实现路由

```R
library(GWmodel3)
library(geojsonsf)

#' Basic GWR
#' @serializer unboxedJSON
#' @post /gwr
function(req) {
  body = req$body
  data = body$data
  depen = body$depen
  indeps = body$indeps
  bw = body$bw
  adaptive = body$adaptive
  if (any(is.null(c(data, depen, indeps, bw, adaptive)))) {
    res$status <- 500
    return(list(msg = "No sufficient arguments"))
  }
  model_formula <- formula(paste(depen, paste(indeps, collapse = "+"), sep = "~"))
  model_data <- geojson_sf(data)
  model <- gwr_basic(model_formula, model_data, bw = bw, adaptive = adaptive)
  list(
    sdf = sf_geojson(model$SDF),
    diagnostic = model$diagnostic
  )
}
```

文件中通过类似于 **roxygen2** 的注释实现路由的设置,支持 GET POST PUT DELETE 等常用方法,也支持 Query String 设置参数。但是由于我们这个场景下参数量比较大(要通过 GeoJSON 的方式传入整个数据文件),所以使用 POST 方法并通过 HTTP 请求的请求体(Body)以 JSON 格式传输数据。

在 **plumber** 中,如果路由有名为 `req` 的参数,则这个参数将自动捕获请求对象。同样,如果有名为 `res` 的参数,则这个参数代表响应对象。在请求对象中,我们可以直接获取 `body` 作为解析后的请求体。经过一系列参数校验和处理,就可以执行我们相应的算法了。

**plumber** 默认使用 **jsonlite** 包的 `toJSON()` 函数将返回值序列化为 JSON 格式字符串。但是由于 R 底层以向量格式存储所有数据,直接解析 `toJSON()` 的返回值得到的结果往往使用起来比较麻烦。这时可以在注释中为路由通过如下方式指定序列化器

```R
#' @serializer unboxedJSON
```

这样得到的响应数据在解析后就非常方便了。

使用下面的命令运行服务器

```bash
Rscript app.R
```

我们可以写一个脚本,用来测试服务器输出

```R
library(sf)
library(GWmodel3)
library(jsonlite)
library(request)
library(geojsonsf)
data("LondonHP")
data_json <- sf_geojson(LondonHP)
body <- list(
  data = data_json,
  depen = "PURCHASE",
  indeps = c("FLOORSZ", "UNEMPLOY", "PROF"),
  bw = 64,
  adaptive = TRUE
)
res <- api("http://127.0.0.1:6000/gwr") %>%
  api_body(body_value = jsonlite::toJSON(body)) %>%
  http(method="POST")

res_sdf <- geojson_sf(res$sdf)
res_sdf
```

API成功执行,得到如下结果

```plaintext
Simple feature collection with 316 features and 14 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: 507400 ymin: 159400 xmax: 552300 ymax: 194900
Geodetic CRS:  WGS 84
First 10 features:
   Intercept    residual    PROF.TV Intercept.SE PROF.SE     PROF  FLOORSZ              geometry FLOORSZ.SE     yhat UNEMPLOY.SE Intercept.TV UNEMPLOY  FLOORSZ.TV UNEMPLOY.TV
1  -174627.7   17000.075 0.04807418      6085658 7630457 366827.9 1474.336 POINT (533200 159400)   376758.8 139999.9    16619161  -0.02869497 715033.1 0.003913209  0.04302462
2  -188388.5  -25462.517 0.04667130      6199661 7750510 361726.4 1726.832 POINT (533300 159700)   403665.0 138962.5    17228214  -0.03038690 691956.9 0.004277883  0.04016417
3  -182290.4  -41494.196 0.04482533      6331307 7903758 354288.5 1789.489 POINT (532000 159800)   423045.9 123244.2    17552711  -0.02879190 600252.3 0.004230012  0.03419713
4  -186506.4  -19445.584 0.04921103      6033409 7538740 370989.2 1597.651 POINT (531900 160100)   382846.4 169445.6    16564237  -0.03091228 745295.2 0.004173086  0.04499424
5  -149628.0    9652.861 0.04558238      6147209 7627975 347701.3 1307.517 POINT (532800 160300)   370839.3 180347.1    16687476  -0.02434081 662406.8 0.003525832  0.03969485
6  -161355.4   -7070.584 0.04603614      5977369 7425576 341844.9 1489.324 POINT (535700 161700)   369344.8 167020.6    16373526  -0.02699438 674865.2 0.004032342  0.04121685
7  -147853.3   13809.725 0.04373267      6343223 7891175 345102.2 1276.782 POINT (535600 161800)   376042.9 136185.3    17193489  -0.02330886 659122.3 0.003395309  0.03833557
8  -167828.1    2121.905 0.04706348      6177821 7734898 364031.2 1417.696 POINT (533400 161900)   377135.1 237828.1    16809147  -0.02716623 687233.4 0.003759120  0.04088449
9  -170397.9   -8811.337 0.04380477      6239154 7875400 344980.0 1537.825 POINT (535500 162200)   401824.0 161811.3    17524101  -0.02731106 698204.3 0.003827112  0.03984252
10 -142768.0 -108816.091 0.04193421      6589694 8164301 342363.5 1239.989 POINT (534200 162500)   387976.5 358766.1    17805151  -0.02166535 651863.5 0.003196041  0.03661095
```

在云计算时代,我们可以通过云计算运营商发布函数计算服务,当有大量计算任务时,或对计算有特殊需求时(比如需要极大的内存、需要GPU等),就可以编写这样一个 RESTful 服务器,挂在函数计算上执行了。函数计算与购买服务器相比,成本会低很多。再结合 Shiny ,甚至可以开发一套前后端分离的网页端应用,前端挂在 GitHub Pages 上,后端挂在函数计算上,就实现了 serverless 应用的开发。