Javascriptで学ぶHTTP通信

# はじめに

本記事は、Webアプリを作るための基礎知識の習得を目的とした学習記事です。主に、

* windows上でExcelVBA等、プログラミングの基礎知識は持っているものの
* Linuxなどサーバは全く扱ったことがなくバックエンドサーバが全くのブラックボックスで
* Webブラウザの挙動がよくわからず、何を使ってどこをどう見れば理解していることになるのか不安

という人を対象にしています。

まず、Webサーバを実装してHTTP通信の実体を確認していきます。

---

# 開発環境の構築

　Windows PCにWebサーバを導入します。Webサーバを作るので処理系をインストール。

## nodejs

 nodejsはJavascriptの処理系です。

*  [nodejs公式](https://nodejs.org/ja/)からwindows用のパッケージをダウンロード
*  展開して実行
*  コマンドプロンプトを開いてnodeと入力
*  終了は.exit

---

## Visual Studio Code(VSCode)

　VSCodeは各種プログラミング言語の統合開発環境です。便利なアドインがたくさんあります。オススメのアドインは次の通りです。

* Bracket Pair Colorizer2 
* Debugger for Chrome 
* (Emacs Keymap) 
* Git graph
* indent-rainbow
* Japanese-language-pack

---

# nodejsによるHTTP通信

 まずは[公式ドキュメント](https://nodejs.org/ja/about/)から始めましょう。書籍を買うのはその後です(基本)。

## ポートlisten

　コンピュータには、通信するための資源として「通信ポート」がOSから提供されています。公式のサンプルコードは3000番を使用しています。

(1) VSCodeで新しいワークスペースを開く; ファイル>名前を付けてワークスペースを保存> httpserverで保存

ワークスペースを新しく作ります

(2) ファイルを編集する; ファイル>新規ファイル

Webサーバプログラムを記述する新しいファイルを作ります。

これはポート3000番でHTTP通信接続を待ち受けるWebサーバプログラムで、"Hello World"をWebブラウザに表示する、というサンプルプログラムです。

サンプルプログラムは、HTTPヘッダであるContent-TypeにMIME-typeである'text/plain'を、HTTPボディに"Hello World"文字列を設定しています。

```server.js
const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;                         // HTTPステータスコード(1)
  res.setHeader('Content-Type', 'text/plain');  // MIME-type(2)
  res.end('Hello World');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});
```

(3) 保存する; ファイル>名前を付けて保存
(4) nodejsプログラムの実行; 実行>デバッグの開始>Node.js

<img width="300" alt="スクリーンショット 2020-06-17 18.01.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192607/ecb50d26-9657-a81e-f9a8-0509e8c730e8.png">

(5) WebブラウザからWebサーバへアクセス; Chromeを起動>http\://localhost:3000と入力> Hello worldと表示される

(6) nodejsプログラムの停止; 右端の四角を押下
<img width="300" alt="スクリーンショット 2020-06-17 18.06.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192607/04b520bf-b8e9-ecf7-e10b-0d08313397cb.png">

(7) WebブラウザからWebサーバへアクセスして拒否される; Chromeに切り替えてリロード  
<img width="400" alt="スクリーンショット 2020-06-17 18.08.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192607/5daa1289-4c6d-c132-91ea-afc5f4eda0ec.png">

--- 

## サーバサイドデバッグ

  nodejsプログラムには、VSCode上で実行を一時停止するブレークポイントが設定できます。

(1) ブレークポイントの設定
(2) Webブラウザでhttp\://localhost:3000を入力すると、VSCode上で設定したブレークポイントで停止
(3) ブレークポイントからステップ実行/再開

---

## クライアントサイドデバッグ

   Webブラウザは二種類の通信をしています。Webサーバに要求した **リクエスト通信**と、応答する**レスポンス通信**です。WebブラウザChromeでは　メニューボタン > その他メニュー > デベロッパーツール > Network を選んでおきます。

<img width="300" alt="スクリーンショット 2020-06-17 18.19.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192607/523c7f76-21e5-3f7b-9f8a-66c476b296f3.png">

(1) HTTPリクエストの実体確認; Webブラウザにhttp\://localhost:3000と入力  > リクエスト通信(localhost)を選択 > Headersタブを選択 > General > Request URL に注目

<img width="373" alt="スクリーンショット 2020-06-17 18.24.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192607/7ad1e4f0-e9ef-94fa-847b-0cb20ef03603.png">

* Headers > Response Headers > view source
* Headers > Request Headers > view source

このview sourceのアンカーをクリックすると、パース前の、通信内容が確認できます。

(2) HTTPレスポンスの実体確認; Responseタブを選択
<img width="310" alt="スクリーンショット 2020-06-17 18.22.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192607/4a41b051-95fd-be8c-5c23-2e5b411ed22f.png">

(3) HTTPステータスコード  

* おなじみの200, 404, 500などあります。詳しくはRFCを参照のこと
* Webサーバに接続に成功して、レスポンス通信が受け取れたら200と思ってください
* 接続に成功しても、サーバ側でエラーが生じた場合は400番台や500番台のステータスコードが返ってきます
* 接続に失敗したら、そもそもHTTP通信が始められなかったということです
  
---

# 実習: Webサーバ実装

サーバサイドのnodejsプログラムを順次書き換えていくことで、HTTPの通信実態を理解します。

## HTTPヘッダContent-TypeとHTTPボディの関係とWebブラウザの解釈

###  MIME-type: text/plain

　HTTP通信は、基本テキストデータを送受信しています。

(1) res.write()

server.jsを書き換えたらデバッグ実行中のメニューで回転矢印のリロードボタンを押すと、デバッグプロセスの停止、ファイル保存、再読込、デバッグの開始と、一連の動作をまとめてやってくれます。

```javascript
const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;                         // HTTPステータスコード(1)
  res.setHeader('Content-Type', 'text/plain');  // MIME-type(2)
  res.write('Hello World');
  res.end();
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});
```


(2) 繰り返し

```javascript
const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;                         // HTTPステータスコード(1)
  res.setHeader('Content-Type', 'text/plain');  // MIME-type(2)
  for(var i = 0; i < 5; i++)
    res.write('Hello World');
  res.end();
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});
```

(3) writeしたからといって<font color="red">直ちに通信しているわけではない</font>(重要)

* res.write行にブレークポイントを仕掛けておく
* http\://localhost:3000/でアクセス
* ブレークポイントで止まったら、ステップ実行
* res.end()で初めてレスポンス通信が開始していることがわかるでしょう

---

###  MIME-type: text/html

(1) HTTPヘッダがtext/htmlの場合、HTTPボディの中身はHTML文書です、という意味です

```server.js
const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;                         // HTTPステータスコード(1)
  res.setHeader('Content-Type', 'text/html');  // MIME-type(2)
  res.write('<html><body><strong>Hello world</strong></body></html>')
  res.end('');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});
```

<img width="200" alt="スクリーンショット 2020-06-18 09.25.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192607/df2f2db4-3775-17bb-59aa-cc84364649bf.png">

WebブラウザはHTML文書として解釈して、Hello worldが強調されていますね。

(2) HTTPヘッダでMIME-typeがtext/plainなのにHTTPボディがHTMLなら...?

```server.js
const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;                         // HTTPステータスコード(1)
  res.setHeader('Content-Type', 'text/plain');  // MIME-type(2)
  res.write('<html><body><strong>Hello world</strong></body></html>')
  res.end('');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});
```

<img width="279" alt="スクリーンショット 2020-06-18 09.23.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192607/3e340aa2-a77f-2220-bfc4-7b1e3f1a5514.png">

HTML文書であっても、text/plainで送るとWebブラウザはHTML文書とは解釈してくれません。HTTPレスポンス通信のsetHeader()で設定されるHTTPヘッダ情報で、Webブラウザ側の挙動が変わることがこれで理解できると思います。

HTTPヘッダの指定で、Webブラウザの挙動を指定できたりします。例えば、ファイルをダウンロードさせる時には、Content-Dispositionヘッダを使用します。おいおい触れていきます。

--- 

## HTTPレスポンスにファイルシステムから読み込んだ内容で通信

(1) サーバサイドでのモジュール組込方法(1); require

モジュールはrequire関数を使います。ここではconfigモジュールを組み込みます。

```server.js
...
const config = require('./config');
...
```

config.jsを新しいファイルとして作成し、exportsという名前のオブジェクトに値を設定します。

```config.js
exports.port = 3000;
```

server.jsは以下の通りになります。

```server.js
const http   = require('http');
const config = require('./config');
const hostname = '127.0.0.1';

const server = http.createServer((req, res) => {
  res.statusCode = 200;                         // HTTPステータスコード(1)
  res.setHeader('Content-Type', 'text/plain');  // MIME-type(2)
  res.write('<html><body><strong>Hello world</strong></body></html>')
  res.end('');
});

server.listen(config.port, hostname, () => {
  console.log(`Server running at http://${hostname}:${config.port}/`);
});
```

(2) ファイルシステムにアクセスする時はfsモジュール; 非同期アクセスでエラーになりがち

```server.js
const http   = require('http');
const fs     = require('fs');
const config = require('./config');
const hostname = '127.0.0.1';

const server = http.createServer((req, res) => {
   fs.readFile(__dirname + '/server.js', 'utf-8', function (err, text) {
        // エラー発生時
        if (err) {
            res.writeHead(404, {'Content-Type' : 'text/plain'});
            res.write('page not found');
            // returnでreadFile関数を抜ける
            return res.end();
        }

        res.statusCode = 200;                         // HTTPステータスコード(1)
        res.setHeader('Content-Type', 'text/plain');  // MIME-type(2)
        res.write(text);
  });
  res.end('');
});

server.listen(config.port, hostname, () => {
  console.log(`Server running at http://${hostname}:${config.port}/`);
});
```

VSCode上でNodeError: Cannot set headers after they are sent to the clientと出る。以下が正解。

```server.js
const http   = require('http');
const fs     = require('fs');
const config = require('./config');
const hostname = '127.0.0.1';

const server = http.createServer((req, res) => {
   fs.readFile(__dirname + '/server.js', 'utf-8', function (err, text) {
        // エラー発生時
        if (err) {
            res.writeHead(404, {'Content-Type' : 'text/plain'});
            res.write('page not found');
            // returnでreadFile関数を抜ける
            return res.end();
        }

        res.statusCode = 200;                         // HTTPステータスコード(1)
        res.setHeader('Content-Type', 'text/plain');  // MIME-type(2)
        res.write(text);
        res.end('');
  });
  // res.end(''); 間違い
});

server.listen(config.port, hostname, () => {
  console.log(`Server running at http://${hostname}:${config.port}/`);
});
```

--- 

# まとめ(講座一時間)

* 生粋のwindowsプログラマ向けにJavascriptでWebサーバ実装
    * nodejs/VSCodeをインストール
    * ポートlisten
    * サーバサイドデバッグ
    * クライアントサイドデバッグ
* 改修実習では、自力でnodejsサーバプログラムをアイデアを出して改修可能になった
    * MIME-type: text/plain
    * MIME-type: text/html
    * HTTPヘッダContent-TypeとHTTPボディの関係
    * レスポンス通信は直ちに発生するわけではない
    * サーバサイドでのモジュール組込方法(1); require
    * ファイルシステムへのアクセス(非同期)    
* 次回の内容はHTTP/GET, POSTでのパラメータ解析です
* 発展的な話題; そのうちやります
    * Content-Typeに指定するMIME-typeが他にどんなものがあるか
    * HTTPステータスコードの意味を押さえよう
    * nodejsプログラムでのモジュール組込を押さえよう
    * nodejsプログラムを保存したら自動でプロセス再起動してもらいたいんだけど...
    * VSCode/launch.jsonで何ができるの?
    * クライアントサイドのデバッグもVSCodeでやりたいんだけど... 

# 参照情報
* [node.js 超入門②webサーバを作る(ルーティングもやってみる)](https://qiita.com/ritukiii/items/8173ff98f31c2f76b39a)