0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

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 kyorohiro@gmail.com
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 の方で続ける予定..

コードは以下

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?