Dart で 作成した Http Server を 公開するまで


https://dev.to/kyorohiro/series/12061 の 翻訳です。

Dart 製の Http Server を VPS上に構築してみる。

Dart では、 dart:io を利用することで、簡単に Http Server のコードが書けます。

  • ./bin/main.dart
import 'dart:io' as io;

void main(List<String> arguments) async {
  try {
    print("start bind");
    var httpServer = await io.HttpServer.bind("0.0.0.0", 80);
    print("binded");
    await for (var request in httpServer) {
      print("receive requested ${request.uri}");
      request.response.write("Hello");
      request.response.close();
    }
  } catch (e, s) {
    print("${e}");
    print("${s}");
  }
}

しかし、これを、 VPS上に設定するとなるともう少し ノウハウが必要です。

ubuntu 20.20

Firewall

まずは Firewall の設定をしましょう!!

$ ufw status
$ ufw default deny
$ ufw allow 22/tcp
$ ufw allow 80/tcp
$ ufw allow 443/tcp
$ ufw enable
$ ufw status
  • port 22 は気をつけてください!! ssh で接続しているのに、Firewall で切断するとアクセスできなくなってしまいます。 詰みますね..

Setup for Systemd

次は、作成した Http Server を 再起動後も動作するDeamon として登録しましょう!!

$ dart2native bin/main.dart 
$ mv bin/main.exe /opt/main.exe

Shell を間に挟んでおきます..

  • ./darthelloserver.sh
#!/bin/sh

#cd /app/hao_dart_server_and_systemd; dart bin/main.dart
/opt/main.exe

そして、Systemd へ登録するための設定を書きます!!

  • ./darthelloserver.service
[Unit]
Description=Dart Hello Http Server
After=syslog.target network-online.target

[Service]
ExecStart = /opt/darthelloserver.sh
Restart = always
Type = simple

[Install]
WantedBy=multi-user.target
WantedBy=network-online.target

準備ができたので、Systemd に登録しましょう!!

$ cp darthelloserver.sh /opt/darthelloserver.sh
$ chmod 655 /opt/darthelloserver.sh
$ cp darthelloserver.service /etc/systemd/system/darthelloserver.service
$ systemctl enable darthelloserver
$ systemctl start darthelloserver

[PS]今回作成したDeamon は systemd-networkd-wait-online があることが前提なので、
念のため、確認して置いてくださいね!!

$ systemctl list-unit-files | grep network
$ systemctl enable systemd-networkd
$ systemctl enable systemd-networkd-wait-online

ここまでのコード


Let's Encrypt から SSL Certification を取得しましょう!!

http のサーバはこれで動作するようになりました。 https にしたいですよね。
まずは、SSL Certification を取得しましょう!!

Certbot を インストール

Let'sEncrypt と通信するために Certbotをインストールしましょう!!

$ apt-get install certbot -y

HttpServer For Certbot

前回作成したHttpServer を修正して、 Certbot に対応します。
Certbotは'${WebRoot}/.well-known/acme-challenge/' に ファイルを作成するので、
'http://${HOST}/.well-known/acme-challenge/xxx' から Get リクエストがあった場合、そのファイルを返すようにする必要があります。

  • bin/main.dart

こんな感じのコード

import 'dart:io' as io;

const String cerbotWebRootPath = "/var/www/html";

void main(List<String> arguments) async {
  try {
    print("start bind");
    var httpServer = await io.HttpServer.bind("0.0.0.0", 80);
    print("binded");
    await for (var request in httpServer) {
      try {
        print("receive requested ${request.uri}");
        if (request.uri.path.startsWith("/.well-known/")) {
          var acmeChallengeFilePath = "" +
              cerbotWebRootPath +
              request.uri.path.replaceAll(RegExp("\\?.*"), "");
          acmeChallengeFilePath = acmeChallengeFilePath.replaceAll("/..", "/");
          var acmeChallengeFile = io.File(acmeChallengeFilePath);
          var acmeChallengeData = await acmeChallengeFile.readAsString();
          request.response.write(acmeChallengeData);
          request.response.close();
        }
        request.response.write("Hello");
        request.response.close();
      } catch (e, s) {
        print("${e}");
        print("${s}");
      }
    }
  } catch (e, s) {
    print("${e}");
    print("${s}");
  }
}

サーバーを更新しましょう!!

$ dart2native ./bin/main.dart
$ mv bin/main.exe /opt/main.exe
$ systemctl restart darthelloserver 

では、Certbot を動かしてみます。

$ certbot certonly --webroot -w /var/www/html -d tetorica.net -m [email protected]
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator webroot, Installer None
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for tetorica.net
Using the webroot path /var/www/html for all unmatched domains.
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/tetorica.net/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/tetorica.net/privkey.pem
   Your cert will expire on 2021-07-02. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

ほい!!
/etc/letsencrypt/live/tetorica.net/ の下に 証明書ができましたね!!

ここまでのコード


SSL に対応させよう!!

無事にSSL Certification を取得できました。
このSSLを読み込んで、 HTTPS サーバーとして動作するようにします。

import 'dart:io' as io;

const String cerbotWebRootPath = "/var/www/html";
const String privkeyPath = "/etc/letsencrypt/live/tetorica.net/privkey.pem";
const String fullchainPath = "/etc/letsencrypt/live/tetorica.net/fullchain.pem";
void main(List<String> arguments) async {
  try {
    print("start bind");
    onRequest(io.HttpRequest request) async {
      try {
        print("receive requested ${request.uri}");
        if (request.uri.path.startsWith("/.well-known/")) {
          var acmeChallengeFilePath = "" +
              cerbotWebRootPath +
              request.uri.path.replaceAll(RegExp("\\?.*"), "");
          acmeChallengeFilePath = acmeChallengeFilePath.replaceAll("/..", "/");
          var acmeChallengeFile = io.File(acmeChallengeFilePath);
          var acmeChallengeData = await acmeChallengeFile.readAsString();
          request.response.write(acmeChallengeData);
          request.response.close();
        }
        request.response.write("Hello");
        request.response.close();
      } catch (e, s) {
        print("${e}");
        print("${s}");
      }
    }

    var httpServer = await io.HttpServer.bind("0.0.0.0", 80);
    print("binded 80");
    httpServer.listen((request) {
      onRequest(request);
    });

    String key = io.Platform.script.resolve(privkeyPath).toFilePath();
    String crt = io.Platform.script.resolve(fullchainPath).toFilePath();
    io.SecurityContext context = new io.SecurityContext();
    context.useCertificateChain(crt);
    context.usePrivateKey(key, password: "");
    var httpsServer = await io.HttpServer.bindSecure("0.0.0.0", 443, context);
    print("binded 443");
    httpsServer.listen((request) {
      onRequest(request);
    });
  } catch (e, s) {
    print("${e}");
    print("${s}");
  }
}

こんな感じ..
反映させましょう。

$ dart2native ./bin/main.dart
$ mv bin/main.exe /opt/main.exe
$ systemctl restart darthelloserver 

試しにブラウザーで開いてみると...
おっ、動きますね!!

SSL の更新方法について

Let's Encrypt の SSL は期限が短いです。
毎月、SSLを更新するに、CRON を設定しておきましょう!!

まずは、Cronが動作してるか確認!!

$ systemctl list-unit-files | grep cron
cron.service enabled enabled

そして、設定!!

$ crontab -u root -e
00 04 03 * * certbot renew && systemctl restart darthelloserver 

ここまでのコード

Isoalte で 分散

Dart は シングルスレッドで動作するため、CPUリソースがあまりそうです。
せっかくのマルチコアの環境ならCPUを、無駄なく利用したいですよね!!

Dart では、 Isolate を利用して、マルチスレッドライクなコードが書けます。

こんにちは Isolate

子プロセス(Isolate)を起動して、文字を表示するだけのコード

import 'dart:isolate' as iso;

onMain(message) async {
  //
  print("child:arg:${message}");
  for (var i = 0; i < 5; i++) {
    await Future.delayed(Duration(milliseconds: 100));
    print("child:print:${i}");
  }
}

main() async {
  iso.Isolate.spawn(onMain, "Hi");
  for (var i = 0; i < 5; i++) {
    await Future.delayed(Duration(milliseconds: 100));
    print("parent:print:${i}");
  }
}
$ dart bin/main_example_isolate_01.dart 
child:arg:Hi
parent:print:0
child:print:0
parent:print:1
child:print:1
parent:print:2
child:print:2
parent:print:3
child:print:3
parent:print:4

Isolate 間の通信

Isolate 間ではメモリーを共有していません。
データをIsolate間で共有したい場合は、 SendPort ReceivePort を利用して
通信を行います。

import 'dart:isolate' as iso;

onMain(message) async {
  //
  print("child:arg:${message}");
  iso.SendPort sendPort = message['p'];
  for (var i = 0; i < 5; i++) {
    await Future.delayed(Duration(milliseconds: 100));
    print("child:print:${i}");
    sendPort.send("hi${i}");
  }
}

main() async {
  iso.ReceivePort receivePort = iso.ReceivePort();
  receivePort.listen((message) {
    print("parent:onMessage: ${message}");
  });
  iso.Isolate.spawn(onMain, {"v": "Hi", "p": receivePort.sendPort});
  for (var i = 0; i < 5; i++) {
    await Future.delayed(Duration(milliseconds: 100));
    print("parent:print:${i}");
  }
  receivePort.close();
}

$ dart bin/main_example_isolate_02.dart 
child:arg:{v: Hi, p: SendPort}
parent:print:0
child:print:0
parent:onMessage: hi0
parent:print:1
child:print:1
parent:onMessage: hi1
parent:print:2
child:print:2
parent:onMessage: hi2
parent:print:3
child:print:3
parent:onMessage: hi3
parent:print:4
child:print:4
parent:onMessage: hi4

子Isolate から 親Isolate にメッセージを送信するだけのコードですね..

もう少し Isolate

他に、pause resuem errorチェック、などのコードは以下を参考にしてください
https://github.com/kyorohiro/hao_dart_server_and_systemd/blob/master/bin/main_example_isolate.dart


Isolate を Dart で作成した Http Server に適用

Isolate を Http Server に適用して、負荷を分散してみましょう!!
Dart の HttpServer オブジェクトは、 Isolate に対応しているので、
実は、そんなにする事はありません。

shared プロパティをTrueにするだけ..

import 'dart:io' as io;
import 'dart:isolate' as iso;

onIsolateMain(message) async {
  var server = await io.HttpServer.bind("0.0.0.0", 8080, shared: true);
  await for (var request in server) {
    print("${request.uri}");
    request.response.write("${message}");
    request.response.close();
  }
}

main() async {
  var server = await io.HttpServer.bind("0.0.0.0", 8080, shared: true);
  server.listen((request) {
    print("${request.uri}");
    request.response.write("parent");
    request.response.close();
  });
  //
  for (int i = 0; i < 10; i++) {
    iso.Isolate.spawn(onIsolateMain, "${i}");
  }
}

うーむ、何もしなくて良い..

$ curl http://tetorica.net:8080/
parent
$ curl http://tetorica.net:8080/
1
$ curl http://tetorica.net:8080/
0
$ curl http://tetorica.net:8080/
4

おわり

続きは、dev.to/kyorohiro/this_series の方で続ける予定..

コードは以下