t-cool

Node.js入門: 導入 - Node.jsでアプリケーションを書くための入門ガイド

Node.js入門: 導入

Node.jsでアプリケーションを書くための入門ガイド


本記事は、原著者の許諾のもと、翻訳・掲載しています。

Getting started with Node.js: Introduction / Nilesh Singh


1989年、Tim Berners-Lee氏が、今日、インターネットとして世に知られているビジョンを提案しました。当時、その提案は十分に受け入れられませんでしたが、彼はWebの初期バージョンを開発する時間を与えられることになり、その結果、HTML、URI、HTTP、ブラウザ(WorldWideWeb)が、初のWebサーバhttpdと共に公開されました。

それ以降、その提案は我々にとって必要不可欠になり、httpdサーバは、今日、Webサービスが書かれる際の中心的なアイデアになりました。

では、サーバとはどのようなもので、なぜWebの設計において必要なのでしょうか。Wikipediaでは、サーバについて、次のように書かれています:

サーバとは、「クライアント」と呼ばれる他のプログラムや機器に、機能を提供するコンピュータプログラムか機器である。

サーバとは、リモートコンピュータであり、ユーザのリクエストを受けて、その結果を返すものです。では、サーバにはどのようなリクエストが入ってきて、また、どのように受け入れるのでしょうか?この部分が、サーバサイドプログラミングが扱うところです。我々はこれまで、Java、Ruby on Rails、Django、PHP、ASP.NET、Apache HTTP Server等を、ビジネスにで用いてきました。それらは、期待にそぐわないと見なされるまでは、すごく良いものでした。

これらのフレームワークは、用いられる目的において優れており、産業界でも広くシェアを獲得してきました。しかし、インターネットが進化するにつれて、よりダイナミックなサーバが必要とされるようになりました。

クライアント側のアプリケーションが数百マイクロ秒ごとにコンテンツを更新するように求めるような、長いポーリングを扱うアプリケーションを作ると仮定しましょう。

先ほどのテクノロジーを用いると、クライアントがリクエストを停止するのを確認するまでスレッドが残るので、スレッドが作られるごとに機能は低下します。

これは、1秒間に数千の新しいクライアントを処理するシステムには対処できるようにみえますが、C10K問題が起こると、サーバは対処できなくなります。

image01.png

複数の機器を使うと、最近のコンテンツのために、サーバにポーリングをすることができます。

Node.jsやNginxのようなイベント駆動の設計では、C10K問題を扱うのにより効率的です。実際、どちらも、その問題を解決するために開発されました。では、Node.jsとはどういうものであり、何ができて、どのように実行しているのかに焦点をあてて見ていきましょう。Nginxについては、後の記事で紹介します。

結局、Node.jsとは何なのか?

Node.jsは、2009年はじめに、Ryan Dahlさんに書かれました。Ryanさんは当時、大量のリクエストを同時に処理できないこと、また、実行ブロックスタイル(逐次プログラミング)を用いる既存のWebサーバに対して批判的でした。Ryanさんは、並行性を念頭に、Google ChromeのオープンソースのJavaScriptエンジンV8とイベントループを用いてNode.jsを書きました。V8とイベントループを組み合わせたことで、Node.jsは、複数の同時リクエストを、シングルスレッド上でI/Oブロッキングなしで処理できるようになりました。しかし、どのようにして実現できたのでしょうか?

イベントループを理解する

Node.jsにおいて、全ては非同期のタスクです。入力スクリプトの初期化からシステムのコールバックにいたるまで、最初から全ては非同期であり、あるタスクが終了しても失敗しても発動(fire)します。

イベントループは、主に、シングルスレッドのJavaScript環境をシステムのカーネルに引き渡すために機能します。

最近の大半のOSは、非同期のタスクを渡すためのカーネルインターフェイスを提供していますが、提供していないOSもあります。そのような場合は、Node.jsにイベントループを提供するLibevというライブラリを使います。Libevは、非同期のI/Oタスクを渡すために、4つのスレッドを作ります。

Node.js内部の働きについて興味があるようでしたら、このチュートリアルは役にたつでしょう。イベントループとその内部の振る舞いについては、別の記事で紹介します。

Node.jsでサーバを起動する

簡単なNode.jsアプリケーションを書くには、すべきことが2つあります。1つ目は、Node.jsが提供するHTTPモジュールをインポートすることです。JavaScript共通のrequireメソッド、もしくは、ECMAScriptのimportを使ってモジュールをインポートした後、createServerメソッドでサーバを初期化する必要があります。createServerメソッドは、コールバック関数を、引数、リクエスト、レスポンスと一緒に登録します。

const http = require('http')
const server = http.createServer((req, res) => {
    res.setHeader('Content-Type', 'text/plain')
    res.end('Hello, this is a sample server!')
})
server.listen(8080)

先ほども言及した通り、Node.jsにおいては全てのコードは、非同期のタスクです。createServerメソッドがイベントループと一緒に登録したコールバック関数は、新しいリクエストがサーバにヒットする度に呼び出されます。そして、リクエストオブジェクトとレスポンスオブジェクトは、スレッドプールかOSカーネルの非同期のインターフェイスによって準備されます。

コールバックの後に続くことは単純明快です。リクエストオブジェクト(req)を受けとり、求められる詳細を引き出し、要求に応じて、レスポンスオブジェクト(res)をリクエストの送り主(クライアント)に返します。本当にそんなに単純なのでしょうか?では、これらのコールバックオブジェクトについて見ていきましょう。

リクエストオブジェクトを理解する

リクエストは、その名前が示すように、サーバにヒットする全てのリクエストを処理するハンドラです。reqは、ただのJavaScriptオブジェクトであり、サーバにリクエストとして認められるのに必要な情報(クライアントのIPアドレス、ホスト名、ヘッダ、ボディー部(JSON等の形式)、パラメータ、クエリ文字等)を含みます。

# リクエストのURLに含まれるパラメータを読む
const params = req.params

リクエストのプロパティを正しくパースして、適切にレスポンスを返すのは、サーバの責任です。Node.jsはリクエストをパースしますが、リクエストオブジェクトを読み、リクエスト-レスポンスのトランザクションを終わらせるのは、プログラマーの仕事です。

リスポンスオブジェクト

トランザクションを完了するために、有効なレスポンスをユーザに送る必要があります。しかし、何が有効で、何が有効でないのでしょうか?また、どのように、また何をレスポンスとして返すのでしょうか?

Node.jsは、そのような場面を対処するために、最低限のAPIを提供しています。リクエストのコールバックとして受け取るレスポンスオブジェクトは、クライアントに応答するために必要な全てのメソッドを保持します。レスポンスヘッダーを設定するためのsetHeader、データをレスポンスのボディー部にアタッチするために用いられるappend、また、レスポンスのプロセスを終わらせるためのend等のメソッドを含みます。

res.setHeader('Content-Type', 'text/plain')

事前に定義されたメソッドを用いることは必ずしも有効であるとは限りませんが、それは確かにエラーを最小限に留めてくれます。

結局、何を受け入れるのか?

最後のパズルのピースは、listenメソッドです。サーバのインスタンスを初期化した後、なぜlistenメソッドが必要なのでしょうか。

ネットワークの授業を受けたことがあれば、クライアントは任意である一方で、サーバーは全てのクライアントにIPアドレスが知られている1つのシステムだと知っているでしょう。全てのクライアントに知らせるために、サーバはlistenとacceptの仕組みを実装します。一度リクエストが確立されると、クエリに保持されて、サーバはアクセプトする準備ができます。

listenメソッドは Node.jsでlistenの部分を担い、createServercallbackは acceptの部分を処理します。listenは4つのパラメータをうけとります。1つ目はNode.jsが動いているポート番号です。2つ目はホスト名であり、サーバ内のプロセスを特定するためのポート番号を伴います。

const PORT = 8080,
      HOSTNAME = '127.0.0.1',
      BACKLOG = 10

server.listen(PORT, HOSTNAME, BACKLOG, () => {
    console.log('Server is now listening on', PORT)
})

3つ目とパラメータは、リスナーのクエリを保留するためのbacklogです。4つ目のパラメータは、リスナーが最初に成功したときに呼び出されるコールバック関数です。

全てのパラメータのlistenの受け入れはオプショナルですが、常にポート番号とホスト名、またbacklogも割り当てておくことが良いです。理由は、そのデフォルト値にあります。もしOSが独自のポートを見つけられない場合や、0に設定された場合、OSは使用されていないポート番号をプロセスに割り当てます。同様のことがホスト名にも当てはまります。もしサーバが独自のホスト名を見つけられない場合、IPv4では(0.0.0.0)、IPv6では(::)で接続のアクセプトを開始します。どちらの値も、プロキシサーバの後ろで動作させるケースや、80番ポートで起動させるといった運用環境では受け付けられません。

上のコードを用いると、どのようなリクエストに対しても、"Hello, this is a sample server!"という文字列を返すサーバを起動することができます。 上のコードを(server.js)という名前で保存して、次のように起動してください:

node server.js

起動すると、リスナーのコールバック関数で定義したように、"Server is now listening on 8080"というログが表示されるでしょう。Node.jsで優れている点は、listenするために、CPUのリソースが食いつぶされないことでしょう。新しいリクエストが処理されるまでの間、じっと休止しています。

では、ここまでのNode.js入門をまとめましょう。Node.jsとは何なのか、またNode.jsがスレッド対応に至った背景、イベントループについて、HTTPサーバの起動方法について見てきました。Node.jsでのリクエストとレスポンスのオブジェクトについても学びました。また、Node.jsが、イベントループの助けをかりて、新しいリクエストをlisten, acceptして、リクエスト-レスポンスんのトランザクションを完了しているかを見てきました。

もし、この記事について、何か問題や質問があれば、下のコメントからお願いします。あなたからのクエリに喜んでリスポンドします。また、もしこの記事が役にたつと思ったら、星ボタンを押していただき、周りの人に勧めて共有してください。