使用Node.js创建瓦片服务器(从mbtiles中传输pbf格式的瓦片)

首先

虽然在矢量瓦片的生产、样式、主机、优化和消费等领域都积累了一些经验,但这里我们想要谈论的是矢量瓦片的主机。

UNVTツールには、数年前に@hfuさんが開発したun-vector-tile-toolkit/onyxというパッケージが含まれており、nodejsを使用してベクトルタイルの配信を実現しています。タイルのホスティングにはtileserver-glやGitHubページなど、いくつかの選択肢がありますが、nodejsサーバーを使用する場合の利点は以下の通りです。

    • シンプルで反応が早い。スケーラブル。

性能のよいサーバーにも適用できるし、ラズベリーパイのような小型PCでも実装可能。

mapboxのnpmモジュールを使うことで、mbtilesからpbfを配信できる。

大容量サイズのデータ配信が容易(このツールは大容量のデータを配信したい場合に向いている。少しのデータならウェブサーバーなどで十分。)。

nodejsのスクリプトと、pm2を組み合わせることで、デーモンプロセスとして実行するのも簡単。

本次任务的目标是什么?

    • onyxのスクリプトをもとにして、nodejs/expressでつくったサーバーを立てる。(その過程を記録する)

 

    わかりやすさのために、ベクトルタイルを配信するモジュールは./route/VT.jsとして切り分ける。

(Onyx也已经开发了大约3年时间。在我使用的服务器上,需要确认每个npm模块的新版本是否能够正常运行,以及确认服务器是否能够在nodejs 16上运行。这次,我创建了一个简单的服务器来进行验证,并顺便做了一些备注。)

环境

我們使用 GitHub,在 Windows 電腦的文本編輯器進行開發。實驗用的 Linux 版本有以下兩種。

環境1:レンタルサーバー(個人の実験用)

OS: CentOS Linux release 7.8.2003
メモリ: 1 GB
nodejs version: 16.13.1  ※nvmで管理
npm version 8.1.2 ※nvmで管理
pm2 version 4.4.0
ドメインは取得済み。SSL/TLS認証はLet’s Encryptで取得。

環境2:ラズベリーパイ(3b)

OS: Debian 11.1
メモリ: 1 GB
nodejs version 16.13.1
ローカルでの利用なので、SSL/TLS認証なし

步骤

步骤1:搭建Node.js服务器(仅静态托管)

首先,我创建了一个名为app0.js的简单文件,内容如下。在进行日志设置和跨域资源共享(CORS)配置之后,我使用express.static(htdocsPath)函数来托管静态内容。(htdocsPath是在配置文件中进行设置的。很抱歉,其中还有一些暂时未使用的变量。)

const config = require('config')
const fs = require('fs')
const express = require('express')
const spdy = require('spdy') //for https
const cors = require('cors') 
const morgan = require('morgan')
//const TimeFormat = require('hh-mm-ss')
const winston = require('winston')
const DailyRotateFile = require('winston-daily-rotate-file')

// config constants
const morganFormat = config.get('morganFormat')
const htdocsPath = config.get('htdocsPath')
const privkeyPath = config.get('privkeyPath')
const fullchainPath = config.get('fullchainPath')
const port = config.get('port') 
const mbtilesDir = config.get('mbtilesDir')
const logDirPath = config.get('logDirPath')


// logger configuration
const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new DailyRotateFile({
      filename: `${logDirPath}/server-%DATE%.log`,
      datePattern: 'YYYY-MM-DD'
    })
  ]
})

logger.stream = {
  write: (message) => { logger.info(message.trim()) }
}

// app
const app = express()
app.use(cors())
app.use(morgan(morganFormat, {
  stream: logger.stream
}))
app.use(express.static(htdocsPath))

//for http
app.listen(port, () => {
    console.log(`Running at Port ${port} ...`)
})

/* for https
spdy.createServer({
  key: fs.readFileSync(privkeyPath),
  cert: fs.readFileSync(fullchainPath)
}, app).listen(port)
*/

除了这个之外,我们还准备了以下类似的config文件(config/default.hjson)。privkeyPath和fullchainPath是用于HTTPS的,所以在第一步暂时不使用。mbtilesDir在第一步也不使用。(可以保持原样)

{
 morganFormat: tiny
 htdocsPath: htdocs
 privkeyPath: ./key/privkey.pem 
 fullchainPath: ./key/cert.pem
 logDirPath: log
 port: 8836
 mbtilesDir: mbtiles
}

以下是我使用的主要npm软件包:

名前version私の感想などconfig3.3.6コンフィグ用に使いますcors2.8.5クロスオリジンでのリソースシェアのため (参考: ベクトルタイルはCORSしよう by @hfuexpress4.17.1nodejsで使えるWebアプリケーションフレームワーク。ルーティングやホスティングが自由にできます。morgan1.10.0httpのログ用ですspdy4.0.2httpsのとき、http2をするのに使います。(Step 1では使いません。)sqlite35.0.2node.js v14までで動くと書いてあるのですが、v16でも一応動きました(v4.2.0 はnodejs16で動きませんでした)。ただ、これが使っているnode-gypの使っているtar(v3.8)には脆弱性があるのでnpm auditで警告が出ます。winston3.3.3これもログ用です。mapbox/mbtiles0.12.1mbtilesからpbfを取り出すのに使います。大事なモジュールなのですが、これが使っているsqlite3には前述の問題があります。また、モジュールの最初にアットマークがつきますが、qiitaでアットマークをつけると勝手にリンクしてしまうので省略しています。(Step1では使いません。)

这个app0.js是在https://github.com/ubukawa/server-test-01 上创建的。您可以使用以下命令来执行它。

git clone https://github.com/ubukawa/server-test-01
cd server-test-01
npm install
node app0.js

只需要这样做,将文件放入htdocs,然后我认为文件将从端口8836进行HTTP主机托管。请尝试访问http(s)://(服务器根目录):8836/index.html,看看是否可见。

2021-12-07-http.png
2021-12-07-server.png

当数据大小较小时,将其以PBF格式静态托管即可,这已足够(理论上不需要使用Node.js服务器)。例如,在这个仓库里,我们将PBF文件放置在htdocs/pbf_tiles文件夹中作为练习用途,你可以通过访问http(s)://(服务器根目录):8836/pbf_tiles/ne-test/{z}/{x}/{y}.pbf来获取瓦片。

步骤2:添加从mbtiles返回pbf瓦片的功能。

在Step 1中创建的简单Node.js服务器上,添加一个路由来从mbtiles返回pbf瓦片。在const app = express()的下方添加var VTRouter = require(‘./routes/VT’)。然后,在稍后的app.use位置添加app.use(‘/VT’, VTRouter)。这样,以/VT/路径发来的请求将被转发到routes/VT.js。

const app = express()
var VTRouter = require('./routes/VT') //tiling
app.use(cors())
app.use(morgan(morganFormat, {
  stream: logger.stream
}))
app.use(express.static(htdocsPath))
app.use('/VT', VTRouter)

然后,准备一个名为routes/VT.js的文件。这个文件通过请求路径确定瓦片的名称、z、x、y,然后使用mapbox/mbtiles(版本0.12.1)的npm模块,从mbtiles中提取pbf瓦片并返回。基本的机制是参考了unvt/onyx。
(详细说明:与onyx不同的是,这里为了简单起见,没有使用空间模块。在满足一个数据源对应一个mbtiles的条件下,我们决定返回pbf。此外,将app.js与模块分开可以使app.js保持简洁,为未来的Azure AD认证添加等准备。)

var express = require('express')
var router = express.Router()
const config = require('config')
const fs = require('fs')
const cors = require('cors')
const MBTiles = require('@mapbox/mbtiles')
const TimeFormat = require('hh-mm-ss')

// config constants
const mbtilesDir = config.get('mbtilesDir')

// global variables
let mbtilesPool = {}
let busy = false

var app = express()
app.use(cors())


//specify the target mbtiles from the path
const getMBTiles = async (t, z, x, y) => {
  let mbtilesPath = `${mbtilesDir}/${t}.mbtiles`
  return new Promise((resolve, reject) => {
    if (mbtilesPool[mbtilesPath]) {
      resolve(mbtilesPool[mbtilesPath].mbtiles)
    } else {
      if (fs.existsSync(mbtilesPath)) {
        new MBTiles(`${mbtilesPath}?mode=ro`, (err, mbtiles) => {
          if (err) {
            reject(new Error(`${mbtilesPath} could not open.`))
          } else {
            mbtilesPool[mbtilesPath] = {
              mbtiles: mbtiles, openTime: new Date()
            }
            resolve(mbtilesPool[mbtilesPath].mbtiles)
          }
        })
      } else {
        reject(new Error(`${mbtilesPath} was not found.`))
      }
    }
  })
}

//Get tile from mbtiles with z,x,y
const getTile = async (mbtiles, z, x, y) => {
  return new Promise((resolve, reject) => {
    mbtiles.getTile(z, x, y, (err, tile, headers) => {
      if (err) {
        reject()
      } else {
        resolve({tile: tile, headers: headers})
      }
    })
  })
}

//GET Tile(router)- t,z,x,y are extracted from the path
router.get(`/zxy/:t/:z/:x/:y.pbf`, 
 async function(req, res) {
  busy = true
  const t = req.params.t
  const z = parseInt(req.params.z)
  const x = parseInt(req.params.x)
  const y = parseInt(req.params.y)

  getMBTiles(t, z, x, y).then(mbtiles => {
    getTile(mbtiles, z, x, y).then(r => {
      if (r.tile) {
        res.set('content-type', 'application/vnd.mapbox-vector-tile')
        res.set('content-encoding', 'gzip')
        res.set('last-modified', r.headers['Last-Modified'])
        res.set('etag', r.headers['ETag'])
        res.send(r.tile)
        busy = false
      } else {
        res.status(404).send(`tile not found: /zxy/${t}/${z}/${x}/${y}.pbf`)
        busy = false
      }
    }).catch(e => {
      res.status(404).send(`tile not found (getTile error): /zxy/${t}/${z}/${x}/${y}.pbf`)
      busy = false
    })
  }).catch(e => {
    res.status(404).send(`mbtiles not found for /zxy/${t}/${z}/${x}/${y}.pbf`)
  })
 }
);

module.exports = router;

这个文件已经上传到了同样的位置https://github.com/ubukawa/server-test-01。如果你将其复制到你自己的服务器上,就可以通过运行node app.js来执行它。

如果在 mbtiles 文件夹中有一个名为 ne-test.mbtiles 的矢量瓦片数据,那么您可以通过以下链接访问:http(s)://(服务器的根地址):8836/VT/ZXY/ne-test/{z}/{x}/{y}.pbf

步骤3:根据需要将网站从HTTP转换为HTTPS。

如果服务器已经获得了域名,并且对该域名进行了SSH / TLS认证,那么很容易将其配置为HTTPS。如果您想尝试,请按照以下第三步进行操作。

    • 事前準備

私は個人のテストサーバーでドメインを取得し、Let’s EncryptでSSL/TLS認証を取得しました。

必要な作業

config/default.hjson で指定したパスに、プライベートキーとCertificateを保存します。(拡張子pem)
app.jsの最後の部分を以下のように修正します。httpの部分をコメントアウトして、httpsの部分のコメントアウトを取ります。

/* for http
app.listen(port, () => {
    console.log(`Running at Port ${port} ...`)
})
*/

// for https
spdy.createServer({
  key: fs.readFileSync(privkeyPath),
  cert: fs.readFileSync(fullchainPath)
}, app).listen(port)

第四步:尝试查看存储库的示例地图。

这个仓库(https://github.com/ubukawa/server-test-01)包含了用于示例的矢量瓦片和Web地图(使用Mapbox GL JS版本1.x的库)。

2021-12-07-http-map.png

步骤5:使用pm2(根据需要)

通过使用pm2,您可以在后台运行nodejs服务器。因为它是一个守护进程,所以即使从服务器注销也可以安全运行。另外,通过使用crontab,您还可以定期重新启动服务器。

2021-12-07-pm2-01.png
2021-12-07-pm2-02.png

整理和课题

2021-12-07-tar.png

在这里,我们确认了基本动作,接下来将按照以下方式发展。

对多个mbtiles文件的支持

在这次的例子中,假设我们有一个向量瓦片源对应一个mbtiles文件,但是当文件变得很大时会变得困难。在un-vector-tile-toolkit/onyx中,我们会为特定的缩放级别(例如ZL6)创建一个mbtiles文件,将全世界分割成多个mbtiles文件进行管理。为了实现相应的瓦片分发,onyx的app.js中包含了路由。另外,在un-vector-tile-toolkit/coesite中,由于区块的大小不一,我们会做一些更复杂的处理。

提供 Azure AD 認證

本日暂时不涉及,但是在这个服务器组件的基础上,还附加了一个可以在nodejs中使用的Azure AD模块(un-vector-tile-toolkit/coesite)。通过Azure AD认证登录后,可以查看Web地图,但是由于添加了认证后,由于跨域(?),瓦片无法共享(可能是因为用户ID是基于会话进行管理),所以还需要进一步努力。

对于HTTP和HTTPS的分析

如果假设有互联网连接,那么使用https是可以的,但是根据(@hfu,2021)关于将树莓派用作服务器的利用,离线环境或本地网络与https之间存在一些复杂关系。特别是对于离线环境下应该使用何种服务器,我们将来需要进一步考虑。

感谢辞等

关于Node.js的托管,我完全参考了@hfu的成果物。非常感谢。
此外,我希望能在这个领域(托管)继续进行一些研究,如果有任何建议、问题等,请随时告诉我,我会非常感激。

请参考

    • 今回のレポジトリ ubukawa/server-test-01

 

    • un-vector-tile-toolkit/onyx

 

    • un-vector-tile-toolkit/coesite

ベクトルタイルはCORSしよう by @hfu

Raspberry Pi のサーバーとしての活用についてby @hfu
npm

bannerAds