5. Node.jsによるサーバ

5.0 目的

ここでは Node.js というツールを用い、JavaScript で Web サーバのプログラムを作成し動作させる。 JavaScriptでは、 Web サーバそのものを JavaScript でプログラミングできる。 Web サーバのプログラムを学ぶことでサーバの動作自体の理解を深めよう。

ここでは、サンプルプログラムは 1.3 サンプルコード に従って、 準備してあるものとする。

また Node.js もインストール済とする。 VS Code で chap05 フォルダーを開くと本章のサンプルプログラムが実行できる。

本単元の前提として 2. Javascript の基本 についてはよく理解しているものとする。

Node.js の機能のうち require, http を使っている。 これらは使われた場所で簡単に説明しているが、 詳細な仕様は Node.js Documentation などを参照することができる。

5.1 シンプルなサーバ

最も簡単なプログラム、server05_01.js を見てみよう。

server05_01.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// server05_01.js
// 簡易 Web サーバー サンプル
// なにがあっても、Hello! World! しか返さない!

// 開始メッセージ
console.log('server05_01 開始')

// http サーバー
var http1 = require('http'); // 受信プログラムを設定
http1.createServer(          // 受信したら動作する
  // メッセージを処理する無名関数
  function (req, msg1) {
    msg1.writeHead(200, {'Content-Type': 'text/plain'});
    msg1.end('Hello! Good bye!\n');
  }
).listen(8080, '127.0.0.1');  // IP とポートを設定する
// url を表示しておく(動作とは無関係)
console.log('http://127.0.0.1:8080/index.html')

server05_01を起動するとデバッグコンソールに以下のように表示される。

_images/server05_01_010.png

'server05_01 開始'という行の下に、起動したサーバのurlが表示される。 表示されたurlをブラウザで開くと以下のように表示される。

_images/server05_01_020.png

異なるブラウザで同じページを開くこともできる。 どのブラウザでも同じように表示される。

実際にサーバとして機能していることを確認するため、 行14を以下のように変更してみよう。

res.end('Simple is the best!\n');

デバッグで停止し、再度デバッグの開始をしすると、ブラウザにも

Simple is the best

と、同じテキストが表示される。

また行14の前に以下 4 行のプログラムを追加してみよう。

var n;
for(n=0; n<10; n++){
  res.write("line"+n.toString(10)+"\n");
}
res.end('Simple is the best!\n');

writeは、応答に対して文字列を出力するメソッドである。 したがって、for文によってくりかえし出力される。 表示は以下のようになる。

line0
line1
line2
line3
line4
line5
line6
line7
line8
line9
Simple is the best!

たった18行であるがサーバとして機能している。 また、サーバ側でプログラムを実行することによって、 動的にコンテンツが作られている。

ところで、同じ url を他のパソコンで指定してもこのページは表示されない。 これは、この Web サーバを外部からアクセスできないような、 設定になっているためである。

設定を変更すれば、この Web サーバを世界中からアクセスすることも可能である。 こんな簡単なサーバでも、インターネットの他の Web サーバとまったくかわらない。 このように簡単な仕組みで世界中に発信できてしまうところが、 インターネットと Web 技術のすごいところだ。

パソコン外部からアクセス可能にする設定方法はやや面倒であるし、 プログラミングの演習のためには、外部からアクセスできる必要がない。 そこで外部からのアクセスのための設定は後で説明することとし、 しばらくの間この設定のまま演習を進める。

5.2 サーバの仕組み

サーバの仕組みを順に見ていこう。

server05_01.js
6
console.log('server05_01 開始')

この行は単にデバッグコンソールにメッセージを出力しているだけで、 サーバの動作としては必要がない。 削除してしまってもサーバとしては問題なく動作する。

server05_01.js
 9
10
var http1 = require('http'); // 受信プログラムを設定
http1.createServer(          // 受信したら動作する

行9~行10はhttpによる通信を設定している。

行9の require はモジュールをロードする関数である。 Node.jsではモジュールをロードすることで機能を拡張できる。 ここでは http モジュールをロードし、その結果を http1 という変数に設定している。 以後 http1 という変数を通じて httpモジュールを使った通信を行うことができる。

行10では http1 に対し createServerというメソッドを呼び出している。 http モジュールではこのメソッドでサーバを起動し、 HTTP プロトコルによるリクエストを受信した場合、 行うべき処理を引数として指定することができる。

server05_01.js
12
13
14
15
  function (req, msg1) {
    msg1.writeHead(200, {'Content-Type': 'text/plain'});
    msg1.end('Hello! Good bye!\n');
  }

その引数が行12からのコードであり、httpの通信を受信した際の処理を設定している。 この処理は、無名関数(i.7 無名関数(Anonymous Function) 参照) によって指定している。

この無名関数には、2つの引数がある。 req はリクエストの種類、 msg1 は http プロトコルにしたがって送受信を行うための変数である。

server05_01.js では req は無視する。 つまり http プロトコルに従ってどんなリクエストやデータが受信されても、 それらを無視して、同じ出力を行う。

まず、msg1.writeHead で HTTPヘッダを送信する。 次に msg1.end で HTTP プロトコルに従って文字列を送信する。 また、msg1.end で送信した場合、送信終了時に、HTTP の送信が終了する。

ここでは使っていないが、 msg1.write を使えば HTTP 送信を複数回に分けて行うことができる。 以下のように msg1.write を使うと、 5 行のテキストを HTTP 送信することができる。 HTTP 送信は、最後に必ず msg1.end で終わらなければならない。

msg1.writeHead(ヘッダ)
msg1.write(1行目)
msg1.write(2行目)
msg1.write(3行目)
msg1.write(4行目)
msg1.end(5行目)

行16では、

server05_01.js
16
).listen(8080, '127.0.0.1');  // IP とポートを設定する

となっているが、これは今作成したサーバにlisten メソッドを適用している。 listenメソッドでは、IP アドレスとポート番号を指定する。

ここでは IP は 127.0.0.1、ポートは 8080 を指定している。 ポートは整数で、IP は文字列で指定することに注意しよう。

createServerで作成したサーバの設定は完了しているが、 まだ HTTP リクエストは受け付けない。 listen メソッドで IP とポート番号を指定して、受信が開始される。

ここでも、行 16の listen メソッドで、 初めて実際にサーバが HTTP request を受付始め、 ブラウザが HTTP リクエストに反応するようになる。

2022/2/13 追記

IPV6で試したいという受講者もいるようだ。あいにく講師の環境は IPV4 なのでテストはできないが、 localhost のアドレスを IPV6 にすることは簡単である。 IPV6 ではlocalhostは [::1] というアドレスになる。 この章のプログラムの 127.0.0.1 の部分を以下のように、::1 または [::1] に書き換えれば、 IPV6 の端末で動作しそうだ。ぜひ試していてほしい。

server05_01v6.js
16
17
18
).listen(8080, '::1');  // IP とポートを設定する
// url を表示しておく(動作とは無関係)
console.log('http://[::1]:8080/index.html')

5.3 フォルダとファイル名の処理

今度は、ipアドレス以下のパスを解釈する機能を追加する。 server05_03.js を起動してみよう。、

server05_03.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// server05_03.js
// 簡易 Web サーバー サンプル
// index.html以外のリンクもたどれる

// IP
var TEST_SERVER_IP = '127.0.0.1';
// Port
var TEST_SERVER_PORT = 8080;

// http サーバ
var http_server  = (require('http')).createServer(); 

// file system
var file_sys = require('fs');

// http_server に 'request' イベントを設定
http_server.on('request', 
    // server のリクエストを処理する無名関数
    function(req, msg1){
        // リクエストがきたら、url を取得
        var url1 = req.url;
        // ファイルを読み取り、その結果によって処理
        file_sys.readFile('.' + url1,'binary',
            // 読み取り結果を処理する無名関数
            function (err, data) {
                // もし err なら 404 エラーを返す
                if(err){ 
                    msg1.writeHead(404, {'Content-Type': 'text/plain'}); 
                    msg1.write('Sagashi monoha nandesuka?\n'); 
                    msg1.end();
                // 正常に読み取れたら、httpヘッダをつけて送信する
                }else{
                    // HTTP プロトコルに従ったヘッダ
                    msg1.writeHead(200, {'Content-Type': 'text/html'}); 
                    // ファイル本体
                    msg1.write(data, "binary"); 
                    msg1.end(); 
                } 
            }
        ); 
    } 
); 

// listen 開始
console.log('server05_03.js 開始'); 
console.log(__dirname);
http_server.listen(TEST_SERVER_PORT, TEST_SERVER_IP); 
// サーバーが動作していることをコンソールに表示する
console.log('http://' + TEST_SERVER_IP + ':' + TEST_SERVER_PORT); 

このプログラムが起動したら、以下urlを開いてみる。

http://127.0.0.1:8080/page1.html

以下ページが表示される。

_images/page1.png

リンクをたどると以下ページが表示される。

_images/page2.png

以後リンクをたどることで両ページを行き来できる。

server05_03.js ではこのように url で指定されたファイルを表示することができる。

プログラムを見てみよう。

server05_03.js
16
17
18
19
20
21
22
23
24
25
26
27
// http_server に 'request' イベントを設定
http_server.on('request', 
    // server のリクエストを処理する無名関数
    function(req, msg1){
        // リクエストがきたら、url を取得
        var url1 = req.url;
        // ファイルを読み取り、その結果によって処理
        file_sys.readFile('.' + url1,'binary',
            // 読み取り結果を処理する無名関数
            function (err, data) {
                // もし err なら 404 エラーを返す
                if(err){ 

行19まで、server05_01.js と同じように処理している。 行21で、受信したHTMLリクエストからurlを取得している。 行23で、urlで指定されたファイルを読み取りしている。 行25~行27では、 その読み取りが成功したか失敗したかにより、 処理を切り替えている。

server05_03.js
27
28
29
30
31
32
33
34
35
36
37
38
                if(err){ 
                    msg1.writeHead(404, {'Content-Type': 'text/plain'}); 
                    msg1.write('Sagashi monoha nandesuka?\n'); 
                    msg1.end();
                // 正常に読み取れたら、httpヘッダをつけて送信する
                }else{
                    // HTTP プロトコルに従ったヘッダ
                    msg1.writeHead(200, {'Content-Type': 'text/html'}); 
                    // ファイル本体
                    msg1.write(data, "binary"); 
                    msg1.end(); 
                } 

行27で、読み取りが成功したか失敗かを判断し、 もしエラーならエラーメッセージをブラウザに送信する。 いわゆる404エラーであるが、エラー番号さえ伝えれば、 ページそのものは独特であってもかまわない。 読み取りが成功したら、つまり指定したurlのファイルが存在すれば、 それをHTTPリクエストの結果として送信する。 つまり指定された HTML ファイルを表示する。

Web ページはその名の通りくもの巣のように相互にハイパーリンクで参照しあっている。 しかし、サーバ側には相互参照の仕組みはない。 指定されたurlからファイルを読み取り出力するだけである。 Webページは相互に複雑に参照しあう構造をとれるから、 サーバにも複雑な仕組みが必要に感じられるが、 実際は url で指定されたファイルを出力するだけでよい。

しかしこのサーバ index.html ファイルがあるにもかかわらず、 以下の url を指定しても、正しく表示されない。

http://127.0.0.1:8080/

また、

http://127.0.0.1:8080/index.html

のように index.html というファイル名を指定しても、 以下のような表示になる。

_images/page05_03.png

一見動いているようだが、よくみると画面の div タグが処理されていない。 つまり stylesheet.css がきちんと処理されていない。

これは server05_03.js では、index.html をデフォルトのファイル名とする処理と、 HTML ファイルにおける contentType がきちんと処理されていないからである。

次の章では、index.html が省略された場合の処理と、 CSS がきちんと処理されるためのサーバ側の仕組みを確認する。

5.4 contentTypeの処理

次に stylesheet.css に対応する。

5.4.1 プログラムの確認

server05_04.js のデバッグを開始し、 ブラウザで以下の url を開いてみよう。

http://127.0.0.1:8080/

以下のように表示される。

_images/page05_04.png

こんどは index.html が自動的に読み込まれ、 stylesheet.css を正しく処理している。

プログラムを見てみよう。

server05_04.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// server05_04.js
// 簡易 Web サーバー サンプル
// html 以外のコンテンツも返す

// ファイル名から content-type を得る
function get_type_str(fn1) { 
    // 拡張子と type_str の辞書配列
    var ext_table = { 
        'html': 'text/html', 
        'htm' : 'text/htm', 
        'css' : 'text/css', 
        'js'  : 'text/javaScript; charset=utf-8', 
        'jpeg': 'image/jpeg', 
        'jpg' : 'image/jpg', 
        'gif' : 'image/gif', 
        'png' : 'image/png'
        };
    var len = fn1.length;            // 長さを取得
    var dot = fn1.lastIndexOf('.'); // 最後の'.'の位置を取得
    var ext2 = fn1.substring(dot + 1, len);//拡張子を得る
        // 拡張子 extension で 辞書 ext_table を引くことで
        // type_str を得る
    var type_str = ext_table[ext2.toLowerCase()]; 
    if(type_str === undefined){ 
        type_str = 'text/plain';
    }; 
    return type_str; 
} 

// IP
var TEST_SERVER_IP = '127.0.0.1';
// Port
var TEST_SERVER_PORT = 8080;

// http サーバ
var http_server  = (require('http')).createServer(); 

// file system
var file_sys = require('fs');

// http_server に 'request' イベントを設定
http_server.on('request', 
    // server のリクエストを処理する無名関数
    function(req, msg1){
        // リクエストがきたら、url を取得
        var url1 = req.url;
        // urlの最後が '/' ならその後に index.html をつける
        url1 = (url1.substring(url1.length - 1, 1) === '/')?url1 + 'index.html' : url1; 
        // ファイルを読み取り、その結果によって処理
        file_sys.readFile('.' + url1,'binary',
            // 読み取り結果を処理する無名関数
            function (err, data) {
                // もし err なら 404 エラーを返す
                if(err){ 
                    msg1.writeHead(404, {'Content-Type': 'text/plain'}); 
                    msg1.write('Sagashi monoha nandesuka?\n'); 
                    msg1.end();
                // 正常に読み取れたら、httpヘッダをつけて送信する
                }else{
                    // HTTP プロトコルに従ったヘッダ
                    msg1.writeHead(200, {'Content-Type': get_type_str(url1)}); 
                    // ファイル本体
                    msg1.write(data, "binary"); 
                    msg1.end(); 
                } 
            }
        ); 
    } 
); 

// listen 開始
console.log('server05_04.js 開始'); 
http_server.listen(TEST_SERVER_PORT, TEST_SERVER_IP); 
// サーバーが動作していることをコンソールに表示する
console.log('http://' + TEST_SERVER_IP + ':' + TEST_SERVER_PORT); 

行51~行66が、ファイルシステムからアクセスしたファイルの処理である。 行51の if ステートメントで err でなければファイルの読み取りに成功したことになる。 そこの場合は行59~行65が実行される。

server05_04.js
59
60
61
62
63
64
65
                }else{
                    // HTTP プロトコルに従ったヘッダ
                    msg1.writeHead(200, {'Content-Type': get_type_str(url1)}); 
                    // ファイル本体
                    msg1.write(data, "binary"); 
                    msg1.end(); 
                } 

行61の

get_type_str(url1)

で、url1に含まれる拡張子により、contentType を求め、

Content-Type

ヘッダを作成している。

拡張子によってcontentTypeを選択する処理は、 このようにサーバが行う。 urlには拡張子を含むファイル名が含まれているが、 それはファイルを読み取る際に使われるだけで、 ブラウザにはファイルの中身と contentType だけが戻される。

5.4.2 連想配列

ここで JavaScript の連想配列を説明する。

他のプログラミング言語では配列の要素を整数で指定する。

x[1] = 3;
x[i] = 12;

といった具合である。

JavaScript では文字列を添え字とすることができる。 これを連想配列と呼ぶ。 連想配列は以下のように記述する。

var a1 = {
  'name':'apple',
  'color':'red',
  'taste':'sweet'
  };

そして 'name', 'color', 'taste' を添え字とすると、 配列の要素として以下のような値が得られる。

a1['name']

'apple'

a1['color']

'red'

a1['taste']

'sweet'

同じ仕組みは辞書、KVP (Key-Value-Pair)と呼ばれることもある。 C言語ではこのようなことはできないが、 モダンなプログラミング言語の多くにとりいれられている。

5.4.3 contentTypeへの変換

拡張子からcontentTypeへの変換は、 行6~行28にあるget_type_str が行っている。

server05_04.js
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function get_type_str(fn1) { 
    // 拡張子と type_str の辞書配列
    var ext_table = { 
        'html': 'text/html', 
        'htm' : 'text/htm', 
        'css' : 'text/css', 
        'js'  : 'text/javaScript; charset=utf-8', 
        'jpeg': 'image/jpeg', 
        'jpg' : 'image/jpg', 
        'gif' : 'image/gif', 
        'png' : 'image/png'
        };
    var len = fn1.length;            // 長さを取得
    var dot = fn1.lastIndexOf('.'); // 最後の'.'の位置を取得
    var ext2 = fn1.substring(dot + 1, len);//拡張子を得る
        // 拡張子 extension で 辞書 ext_table を引くことで
        // type_str を得る
    var type_str = ext_table[ext2.toLowerCase()]; 
    if(type_str === undefined){ 
        type_str = 'text/plain';
    }; 
    return type_str; 
} 

get_type_str 関数では 連想配列 が使われている。 行8~行17で拡張子を contetType に変換するための 連想配列 が定義されている。

server05_04.js
 8
 9
10
11
12
13
14
15
16
17
    var ext_table = { 
        'html': 'text/html', 
        'htm' : 'text/htm', 
        'css' : 'text/css', 
        'js'  : 'text/javaScript; charset=utf-8', 
        'jpeg': 'image/jpeg', 
        'jpg' : 'image/jpg', 
        'gif' : 'image/gif', 
        'png' : 'image/png'
        };

そして行23でその連想配列を使って、拡張子をcontentTypeに変換している。

server05_04.js
23
    var type_str = ext_table[ext2.toLowerCase()]; 

5.4.4 スタートアップファイルの処理

server05_04.js ではファイル名を省略した場合に、 index.html というファイル名を補っている。 以下で index.html というファイル名をおぎなっている。

server05_04.js
48
        url1 = (url1.substring(url1.length - 1, 1) === '/')?url1 + 'index.html' : url1; 

この index.html というファイル名を補う処理も。 サーバが行うべき処理である。 省略時に選ばれるファイルを、スタートアップファイルと呼ぶ。 多くのサーバでは設定ファイルによって指定することができる。 server05_04.js においては、 この部分のプログラムを修正すれば、 別のスタートアップファイルを選ぶこともできる。

5.5 Web ページを公開する

5.5.1 なぜ他のパソコンで開けないか

これまで VS Code とサーバプログラムを実行しているパソコンで、 Web ブラウザを使って http://127.0.0.1/ を表示していた。 しかし、他のパソコンで同じ http://127.0.0.1/ を開こうとしても Web ページは開かない。 これは 127.0.0.1 は他パソコンからは接続できないアドレスだからだ。

一般に、127.0.0.1 というIPアドレスは localhostという特別なアドレスとされている。 localhostはそのコンピュータの中からだけ見えるアドレスであり外部からは見えない。

サーバのプログラム自体はどんなIPアドレスでも対応している。 以下のようにしてサーバに外部から接続できる。

  1. サーバを動かす PC のIPアドレスを確認する

  2. サーバをそのアドレスに応答するよう設定する

  3. Windowsファイヤーウォールで Node.js の設定を ON にする。

5.5.2 IPアドレスの確認

設定/ネットワークとインターネット/状態を選ぶ。

_images/set05_05_030.png

①の接続プロパティの変更を選び、プロパティを下方向にスクロールする。

_images/set05_05_040.png

IPv4 アドレス、という欄に IP アドレスがある ここではwifi接続をしているものとすると、 このIPアドレスはwifiルータで接続されたLANの中で有効なアドレスで、 wifi 接続をしなおすと変化するので、再度確認する必要がある。 同じwifiルータで接続した機器の間で有効である。

サーバプログラムのIPアドレスの値を、このアドレスに変更する。

IPv4 アドレス、という欄のアドレスをメモしておく。 たとえば IP アドレスが 192.168.1.3 であれば行30-行31を以下のようにする。

// IP
var TEST_SERVER_IP = '192.168.1.3';

これで、サーバは 192.168.1.3 への通信に応答するようになる。 ただし、ファイアウォールが通常はまだ閉じているから、 これだけではサーバは外部からのアクセスに応答しない。

5.5.3 Windowsファイアウォールの設定

Windowsファイアウォールというのは Windows パソコンを外部の攻撃から守るための仕組みである。 ネットに接続しても、パソコン内のプログラムが外部からのデータを受け取らなければ、 安全性はたもたれる。 そこで、Windows ファイアウォールでは、接続すべき(データを受信する)対象を制限する。

Node.js は通常は使わないので、切断されており、 Windows ファイアウォールで Node.js による接続を有効にすることで、 初めて Node.js がインターネットとデータを入出力できるようになる。

設定するためにはまず、先と同様に、

設定/ネットワークとインターネット/状態を選び、 下記画面で②のWindowsファイアウォールを選ぶ。

_images/set05_05_030.png

ファイアウォールによるアプリケーションの許可、を選ぶ。

_images/set05_05_050.png

スクロールして

Node.js: Server-Side JavaScript

を選び、設定の変更をクリックしてから Node.js のチェックボックスをチェックし、 OKをクリックして設定を有効にする。

_images/set05_05_060.png

この後サーバを実行し、 同じwifiルータに接続したスマートホンなどで、 サーバの Webページを表示することができる。

たとえば IP アドレスが192.168.1.3であれば、 スマホのブラウザで、

192.168.1.3:8080

を指定すれば以下のように Web ページが表示される。

_images/set05_05_070.jpg

サーバのプログラムのテストが終わったら、 ファイアウォールの Node.js の設定を、 元に戻す。 以下のように設定の変更で Node.js のチェックボックスを外し、 OKをクリックして設定を有効にする。

_images/set05_05_080.png

これで再び Node.js はインターネットからのデータを受け付けなくなる。

5.5.4 インターネットへの公開

さて、この方法でも、同じ wifi の範囲をこえてはサーバをアクセスできない。 通常 LAN に接続したサーバにはLAN内のIPアドレスが割り当てられ、 LANの外側のインターネットからは接続できない。 それでは、サーバはどうすればインターネットに公開できるだろうか?

一つの方法は、LAN上のサーバをインターネットからアクセス可能とするためのルータの設定をすることだ。 しかし、この方法はやや複雑であり、本講座の範囲を超えるので、もう一つの方法を紹介する。

もっとも簡単なのは、完成したWebサイトをレンタルサーバ上でデプロイ(稼働)する方法である。 現在多くのレンタルサーバが Node.js を含むサーバのイメージをデプロイできる。 VS Code はサーバの設定がわかれば、VS Code 上で動作しているサーバを、 そのままレンタルサーバにアップロードしてデプロイできる。

レンタルサーバの維持費は月にコーヒー一杯程度で済む場合が多い。 こうして公開したサーバでも、全世界の利用者にサービスを提供することができる。

思いがけず人気サイトになったら、サイトの公開が株式上場につながるかもしれない。

ただし、全世界にサービスを提供することで、 著作権侵害や、個人情報の扱いにも、 重大な責任が生じる。 サービスの適法性については、もちろん十分注意が必要である。

5.6 まとめ

  1. Node.js では簡単にサーバを動作させることができる

  2. サーバの基本動作は HTTP リクエストに応答することである

  3. contentType を処理することで HTML や text を含む様々なファイル形式に対応する

  4. LANからパソコン上のサーバに接続するには、IPアドレスを設定し、firewallを開く必要がある

  5. レンタルサーバ上でサーバをデプロイすればインターネットに公開できる。その場合には様々な規制や保護される権利があるので、十分に注意する必要がある。