【第6回】Webサーバーを書いてみよう

Node.jsを使って簡単なWebサーバーを作成しアプリを作ってみましょう。

Homeブログ一覧【第6回】Webサーバーを書いてみよう

はじめに

今回からNode.jsを使うためNode.jsの導入が必要です。インストールがまだの方は先に進む前にインストールを行ってください。

また、今回のWeb研からGitを用いてコードを管理します。Git(GitHub)の講習会に参加していない方は、【第4回】Git(GitHub)入門を参照してください。

Webサーバーとは

Webサーバーとは、インターネット上でWebページを提供するためのシステムのことです。

例えば、Webブラウザ(Chrome, Firefox, Safari, Edgeなど)を使ってwww.example.comというサイトを見ようとする時に、何が起きているでしょうか?

ブラウザがwww.example.comにアクセスすると、www.example.comのサーバーにリクエストが送られます。そして、サーバーがリクエストを受け取り、HTMLファイルやその他(画像、CSS、JavaScriptなど)のファイルが返されて、データが表示されます。

サーバー
クライアント
HTTP通信
データの要求/操作
データの返却
アプリケーションサーバー
データベース
ブラウザ

HTTPサーバーを書いてみよう

まず、http-server-introという作業ディレクトリを作りましょう。

作業ディレクトリ内で、Gitリポジトリを初期化しましょう。

git init

package.jsonというファイルを作成し、以下のように記述して下さい。

package.json
{
  "name": "http-server-intro",
  "type": "module"
}

server.jsというファイルを作成し、以下のコードを書いてください。

server.js
import http from "http";

const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello, Node.js!");
});

server.listen(3000);

サーバーを起動してみましょう。

node server.js

http://localhost:3000にアクセスすると、Hello, Node.js!と表示されます。

これで、Web(HTTP)サーバーが作成できました。

タスクをキルするには、Ctrl + C(Windows)またはCommand + C(Mac)を押してください。

作成したファイルをGitリポジトリに追加し、コミットしましょう。

git add .
git commit -m "Webサーバーを作成し、Hello, Node.jsと表示する"

git add .を実行することで、全てのファイルを追跡対象としています。 追跡対象のファイルに変更があるため、 git commit でその変更履歴を記録しています。

解説

import http from "http";

httpモジュールをインポートしています。httpモジュールはNode.jsに標準で組み込まれているモジュールで、HTTPサーバーを作成するための機能を提供しています。

このモジュールを使わないと、プロトコル(通信の約束事)の実装やソケット通信(通信用インターフェース)の実装など、低レイヤーから入らなければならず、とてもめんどくさいです。そのためhttpモジュールやexpressなどのモジュールを使うことが一般的です。

const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello, Node.js!");
});

http.createServerメソッドは、HTTPサーバーを作成するメソッドです。引数にはリクエストを処理するコールバック関数を指定します。コールバック関数の引数には、リクエスト情報が格納されたオブジェクト,レスポンスに使うオブジェクトが渡されます。

res.writeHeadメソッドは、レスポンスヘッダを書き込むメソッドです。引数にはステータスコードとヘッダ情報を指定します。ステータスコード200は「OK」を表し、Content-Typeヘッダにはtext/plainを指定しています。

ステータスコードとは

HTTPレスポンスステータスコードとは、特定のHTTPリクエストが正常に処理されたかどうかを示します。

主なステータスコードは以下の通りです。

ステータスコード意味
100-199情報レスポンス
200-299成功レスポンス
300-399リダイレクト
400-499クライアントエラー
500-599サーバーエラー

皆さんも見かけたことがあるかもしれないステータスコードを挙げてみます。

  • 404 Not Found:リクエストされたリソースが見つからないことを示します。(URLが間違っていたり、リダイレクトの設定ミスなど)
  • 403 Forbidden:リクエストされたリソースにアクセス権限がないことを示します。
  • 500 Internal Server Error:サーバー内部でエラーが発生したことを示します。
  • 502 Bad Gateway:サーバーの通信状態に問題があることを示します。
  • 200 OK:リクエストが成功したことを示します。
server.listen(3000);

server.listenメソッドは、HTTPサーバーが指定したポートでリクエストを待ち受けるメソッドです。引数にはポート番号を指定します。

package.jsonとは何か

package.jsonは、使用したライブラリやそのバージョン、ライブラリの依存関係などの情報が詰まった、Node.jsプロジェクトの設定ファイルです。

簡単に言うと「アプリ開発の際に自分がインストールして使ったライブラリやそのバージョンを記録しておくファイル」です。

以下のコマンドを実行するか、自分で作成することができます。

npm init

このファイルは、複数人で共同開発をするときなどに、使用しているライブラリやそのバージョンなどをそろえた環境で開発を行うことが望ましいためです。

ライブラリのバージョンが異なると、コードの書き方が変わっていたり、正常に動作しないことがあったりするためです。

サーバーにリクエストを送信してみよう

リクエストとは、クライアント(ブラウザ)がサーバーに送信する要求のことです。

リクエストの種類

HTTPリクエストには、主に以下のような種類があります。

メソッド説明
GETリソースの取得ブラウザでWebページを表示するとき
POSTリソースの作成フォームからデータを送信するとき
PUTリソースの更新ファイルなどを送信内容で上書き更新するとき
DELETEリソースの削除ファイルなどを削除するとき

リクエストを送信する方法には、ブラウザを使う方法以外にも、curlコマンドやPostmanなどのツールを使う方法があります。

curlコマンドを使ってリクエストを送信してみよう

先ほどのserver.jsを以下のように変更してください。

server.js
import http from "http";

const server = http.createServer((req, res) => {
  // レスポンスのヘッダーを作成、今回はいつでもただのテキストを返したいので先に作っておく
  const headers = {
    "Content-Type": "text/plain",
  };
  // リクエストのメソッドによって処理を分岐
  if (req.method === "GET") {
    res.writeHead(200, headers);
    res.end("Hello, World!\n");
  } else if (req.method === "POST") {
    res.writeHead(200, headers);
    res.end("Received POST request\n");
  } else {
    res.writeHead(404, headers);
    res.end("404 Not Found\n");
  }
});

// サーバーを起動
server.listen(8000, () => {
  console.log("Server running on port 8000");
});

サーバーを起動してみましょう。

# &を付けてバックグラウンドで実行
node server.js &

curlコマンドを使ってリクエストを送信してみましょう。

# GETリクエスト
curl http://localhost:8000

Hello, World!と表示されます。

# POSTリクエスト
curl -X POST http://localhost:8000

Received POST requestと表示されます。

# DELETEリクエスト
curl -X DELETE http://localhost:8000

DELETEリクエストは条件に合致しないため、404 Not Foundと表示されます。

タスクをキルして終了させましょう。

# バックグラウンドで実行しているタスクをフォアグラウンドに戻す
fg

Ctrl + C(Windows)またはCommand + C(Mac)を押してください。

それでは、編集したファイルをコミットしましょう。

git add .
git commit -m "GETとPOSTリクエストを受け取るサーバーを作成"

解説

if (req.method === "GET")

req.methodはリクエストのメソッドを表し、GETメソッドでリクエストが送信されたときに実行されます。

ルーティングを実装してみよう

ルーティングとは、リクエストされたURLに応じて処理を振り分けることです。

server.jsを以下のように変更してください。

server.js
import http from "http";

// ユーザーのリストを定義
const userList = [
  { id: 1, name: "Alice", county: "USA", age: 19 },
  { id: 2, name: "Bob", county: "Canada", age: 25 },
  { id: 3, name: "Ken", county: "Japan", age: 22 },
];

const server = http.createServer((req, res) => {
  if (req.url === "/" && req.method === "GET") {
    res.writeHead(200, { "Content-Type": "text/plain" });
    res.end("Hello, World!\nAccess to /users to get user data!");
  }
  if (req.url === "/users" && req.method === "GET") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(userList));
  }
});

server.listen(8000, () => {
  console.log("Server is running on port 8000");
});

サーバーを起動してみましょう。

node server.js

ブラウザなどでhttp://localhost:8000にアクセスすると、/(ルート)というURLにアクセスした場合のメッセージが表示されます。

次に、http://localhost:8000/usersにアクセスしてみましょう。

/usersというURLにアクセスした場合、userListというJSON形式のデータが応答(レスポンス)として返され、表示されます。

それでは、編集したファイルをコミットしましょう。

git add .
git commit -m "ルーティングを実装し、ユーザーリストをJSON形式で返す"

解説

if (req.url === "/" && req.method === "GET")

req.urlはリクエストされたURLを表します。リクエストのメゾットがGETで、/にアクセスしたときに実行されます。

if (req.url === "/users" && req.method === "GET")

同様に/usersにアクセスしたときに実行されます。

res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(userList));

application/jsonとは、レスポンスにおいてJSON形式のデータを返すことを示します。

JSON形式のデータを扱ってみよう

ここで、JSON形式のデータって何?となるかもしれません。

JSON(JavaScript Object Notation)は、JavaScriptにおけるオブジェクトの書き方を参考に作られたデータフォーマット(データの記述形式)です。

世界中の様々なサイトはJSONを使った通信が使われており、現状一番ベーシックなデータ通信の方法だと言えると思います。

(詳しくはAPI Technologies - The State of API Technologies in 2021 | Postmanを参照)

JSONは軽量かつ可読性が高く、APIを用いてWebサイトからデータを取得する際によく使われます。ちなみに、序盤に作成されたpackage.jsonもJSON形式で記述されています。

先ほど返されたuserListは以下のようなJSON形式のデータです。

[
  {
    "id": 1,
    "name": "Alice",
    "county": "USA",
    "age": 19
  },
  {
    "id": 2,
    "name": "Bob",
    "county": "Canada",
    "age": 25
  },
  {
    "id": 3,
    "name": "Ken",
    "county": "Japan",
    "age": 22
  }
]

他のデータフォーマットとの違い

  • CSV(Comma-Separated Values):カンマで区切られたテキストデータ
id,name,county,age
1,Alice,USA,19
2,Bob,Canada,25
3,Ken,Japan,22
  • XML(eXtensible Markup Language):HTMLと同じようにデータをタグで囲む形式
<users>
  <user>
    <id>1</id>
    <name>Alice</name>
    <county>USA</county>
    <age>19</age>
  </user>
  <user>
    <id>2</id>
    <name>Bob</name>
    <county>Canada</county>
    <age>25</age>
  </user>
  <user>
    <id>3</id>
    <name>Ken</name>
    <county>Japan</county>
    <age>22</age>
  </user>
</users>

TODOアプリを作ってみよう

新しくsimple-todo-appという作業ディレクトリを作りましょう。

Gitリポジトリを初期化しましょう。

git init

package.jsonというファイルを作成し、以下のように記述してください。

package.json
{
  "name": "simple-todo-app",
  "type": "module"
}

server.jsというファイルを作成し、以下のコードを書いてください。

server.js
import http from "http";
import { StringDecoder } from "string_decoder"; // 文字列をデコードするためのモジュール

// TODOリストを定義
const todoList = [
  { title: "JavaScriptを勉強する", completed: false },
  { title: "TODOアプリを自作する", completed: false },
  { title: "漫画を読み切る", completed: true },
  { title: "ゲームをクリアする", completed: false },
];

const server = http.createServer((req, res) => {
  const decoder = new StringDecoder("utf-8"); // 文字列をデコードするためのインスタンス
  let buffer = ""; // リクエストデータを空文字列で初期化

  req.on("data", (data) => {
    buffer += decoder.write(data);
  });

  req.on("end", () => {
    buffer += decoder.end();

    const headers = {
      "Content-Type": "application/json",
    };

    if (req.method === "GET" && req.url === "/") {
      res.writeHead(200, headers);
      res.end(JSON.stringify(todoList));
    } else if (req.method === "POST" && req.url === "/") {
      try {
        const data = JSON.parse(buffer);
        // タイトルが空の場合はエラーを返す
        if (!data.title) {
          throw new Error("Title must be provided");
        }

        // 新しいTODOのタイトルと完了状態を定義
        const newTodo = {
          title: data.title,
          completed: !!data.completed,
        };

        // 配列todoListに新しいTODOを追加(push)する
        todoList.push(newTodo);
        // 正常に処理されたことを返す
        res.writeHead(200, headers);
        res.end(JSON.stringify({ message: "Successfully created" }));
      } catch (err) {
        // 400 Bad Requestを返す
        res.writeHead(400, headers);
        // エラーメッセージを返す
        res.end(JSON.stringify({ message: err.message }));
      }
    } else {
      res.writeHead(404, headers);
      res.end(JSON.stringify({ message: "Not Found" }));
    }
  });
});

server.listen(8000, () => {
  console.log("Server running on port 8000");
});

次に、以下のコマンドを実行してください。

node server.js &

http://localhost:8000にアクセスすると、TODOリストが表示されます。

新しいTODOを追加してみよう

curlを使ってPOSTリクエストを送信してみましょう。

curl -X POST -H "Content-Type: application/json" -d '{"title": "次回のWeb研に出席する"}' http://localhost:8000

ページをリロードすると、新しいTODOが追加されていることが確認できます。

タスクをキルして終了させましょう。

# バックグラウンドで実行しているタスクをフォアグラウンドに戻す
fg

Ctrl + C(Windows)またはCommand + C(Mac)を押してください。

それでは、編集したファイルをコミットしましょう。

git add .
git commit -m "POSTリクエストを送ることでTODOを追加できるようにした"

解説

import { StringDecoder } from "string_decoder";

string_decoderモジュールをインポートしています。string_decoderモジュールは、リクエスト内容が"UTF-8"という文字コードで送られてくることを期待し、送られてきたバイト列(Buffer)を変換するためのものです。

req.on("data", (data) => {
  buffer += decoder.write(data);
});

クライアントからデータを受け取ったときに、dataイベントが発生します。データを受け取り、bufferに追加しています。

req.on("end", () => {
  buffer += decoder.end();
  // 以下略
});

データの受け取りが完了したときに、endイベントが発生します。データの受け取りが完了したら、bufferに追加していたデータをdecoder.end()でデコード(文字列に変換)し、bufferに格納します。

if (req.method === "GET" && req.url === "/") {
  res.writeHead(200, headers);
  res.end(JSON.stringify(todoList));
}

GETメソッドで/にアクセスしたときに、todoListをJSON形式で返します。

todoList.push(newTodo);

todoListという配列にpushで、末尾に新しいTODOを追加します。

try {
  // 例外が発生する可能性のある処理
} catch (err) {
  // 例外が発生したときの処理
}

try...catch文は、例外が発生する可能性のある処理をブロックで囲み、例外が発生したときには別の処理を行うための文です。

例外処理の詳しい解説

JSではthrowという命令でプログラムの実行を止めてスコープを遡ることができます。

基本はthrowされると大域(一番上のスコープ)まで遡ってプログラムが終了しますが、遡るときにtry文があれば、そこで遡るのをやめ、catchを実行するようになっています。

基本的にjsの関数はエラーをthrow new Error("エラーメッセージ")として表現してくれているため、エラーが起きるかもという箇所をtryブロックで囲っておくことで例外処理ができるという仕組みです。

サンプルコード

// 50%の確率でエラーを発生させる害悪関数
const greet = () => {
  if (Math.random() > 0.5) {
    throw new Error('もう昼だぞ!');
  } else {
    console.log('おはようございます');
  }
};

// エラーをキャッチしてないのでそのままプログラムが停止する
greet();

// エラーをキャッチしてプログラムを停止させないようにする
try {
  greet();
} catch (e) {
  console.log('エラーが発生したらしいぞ');
  console.error(e);
}

まとめ

今回はNode.jsを使ってWebサーバーを作成し、ルーティングについて学習したり、JSON形式のデータを扱ってみたりしました。これまでに紹介したJavaScriptの文法に加えて、今回は少し実践的なプログラムに触れたため、慣れない部分も多いと思います。そういった方は、基礎的な文法はマスターする必要はありますが、細かい部分まで完璧に覚える必要はありません。コピペで実装しても為になると思います。

次回は、 Node.js より簡潔にコードを書くことができる Hono というフレームワークを使って、TODOアプリを作る予定です。