SlideShare a Scribd company logo
Node.js 入門
                       2011 年 4 月 16 日


                             森 俊夫 @ 徳島
                     forest1040@gmail.com
            http://d.hatena.ne.jp/forest1040/




                                         1
自己紹介
   id:forest1040  です。
   徳島で、フリーランスをやってます。
   Web 系エンジニアです。最近は、 Java EE(JBoss 
     Seam) と Ruby をよく使っています。
   1年前に息子が生まれ、イクメン中です。
   Node.js on Android をやってます。




                                       2
アジェンダ
   基礎編
            Node.js とは
            非同期 I/O とイベントループ
            Node.js のアーキテクチャ
   実践編
            インストール
            デバッグ環境
            Node.js を使ったリアルタイム通信


                                    3
Node.js とは
   サーバサイド JavaScript
   Google の V8 エンジン搭載
   シングルスレッド非同期 I/O 環境
   イベントループモデル




                           4
シングルスレッド非同期 I/O 環境
           と
        イベントループ




                         5
頭の体操(並行処理)

    突然ですが、クイズです。
   シングルスレッドで並行処理を行うには?
   マルチスレッドとシングルスレッドの
    並行処理の違いは?




                          6
並行処理
   マルチスレッド( multithread )による並行処理
    呼び元と並行に処理が行われる。
   シングルスレッドでのコールバック( callback )によ
     る並行処理
    呼び元がプロセッサを使用していないときに処理
     が行われる。




                                     7
非同期と同期、ノンブロッキングとブロッキング

   さらにクイズです。
    非同期とノンブロックの違いがわかります
     か?




                          8
非同期とノンブロッキング
   実は、非同期( Asynchronous )とノンブロッキング
     ( non­blocking )は同じ意味としてよく使われて
     いる。
   日本語に惑わされると負け。




                                      9
非同期(ノンブロッキング)とは
    非同期呼び出し( Asynchronous Call )と同期呼び
     出し( Synchronous Call )の違い
   同期呼び出し( Synchronous Call )
    通常メソッドを呼び出すとメソッド内の処理が完了
     するまで、呼び出し元には戻ってこない。このよう
     なメソッド呼び出しのこと。
   非同期呼び出し( Asynchronous Call )
    メソッドを呼び出した瞬間に呼び出し元に処理が
     戻ってくるような呼び出しのこと。非同期で呼び
     出されたメソッドは、環境によって処理されるタイ
     ミングが変わる。   まさしくノンブロッキング
                                    10
ノンブロッキング I/O とは
    ブロッキング I/O ( blocking I/O )とノンブロッキン
     グ I/O ( non­blocking I/O )の違い
   ブロッキング I/O ( blocking I/O )
    データ処理が完了するまで待たされること。
   ノンブロッキング I/O ( non­blocking I/O )
    データ処理の完了を待たされずに、他の処理を行
     えること。



                                          11
I/O モデル
     W. Richard Stevens の
     「 UNIX ネットワークプログラミング 第 2 版 Vol.1 」
   Synchronous I/O Operation ( 同期 I/O 操作 )
              blocking I/O ( ブロッキング I/O)
              non­blocking I/O ( 非ブロッキング I/O)
              I/O multiplexing (I/O 多重化 )
              signal driven I/O ( シグナル駆動 I/O)
   Asynchronous I/O Operation ( 非同期 I/O 操作 )
              Asynchronous I/O ( 非同期 I/O)

                                                 12
Boost application performance using 
           asynchronous I/O




引用: Boost application performance using asynchronous I/O
http://www­128.ibm.com/developerworks/linux/library/l­async/
                                                               13
Boost application performance using 
           asynchronous I/O



 この図では、 I/O multiplexing が
Asynchronous に分類されている。




引用: Boost application performance using asynchronous I/O
http://www­128.ibm.com/developerworks/linux/library/l­async/
                                                               14
C10K 問題
   ハードウェアの性能上は問題がなくても、あまりにもクライアントの数
     が多くなるとサーバがパンクする問題のこと。
    ◇ 解決方法
1. 各スレッドが複数のクライアントを受け付ける。 そしてノンブロッキン
    グ I/O  と レベル・トリガ型の完了通知 (level­triggered readiness 
    notification) を利用する。
2. 各スレッドが複数のクライアントを受け付ける。 そしてノンブロッキン
    グ I/O  と 変更型の完了通知 (readiness change notification) を利
    用する。
3. 各スレッドが複数のクライアントを受けつける。 そして非同期 I/O  を
    使う。
4. 各スレッドが一つのクライアントを受けつける。 そしてブロッキング
    I/O  を使う。
5. サーバのコードをカーネルに組込む。

                                                      15
マルチスレッド vs  ノンブロッキング
   apache  マルチスレッドモデル (C10K の解決案 No.4)
   nginx  ノンブロッキング( C10K の解決案 No.1 )
    1秒あたりの処理リクエスト数                                 メモリ使用量




    nginx  は、最高で秒間 10,000 リクエストを処理する。同時接続数を
      増やしても、リクエスト処理数は少々減る程度
   apache の場合、同時接続数を増やすと著しくリクエスト処理数が減
      る。しかも、メモリ使用量が同時接続数に比例して増える。
    つまり、同時接続数が多い場合は、ノンブロッキングが有利!!
参照: http://blog.webfaction.com/a­little­holiday­present
                                                            16
Node.js の場合
   「 3.  各スレッドが複数のクライアントを受けつける。 そして
      非同期 I/O  を使う」を採用
   「 libev 」と「 libeio 」を使用して、非同期 I/O 環境を実装
   libev
    C 言語で書かれたイベントループライブラリ
      イベントループとは、無限ループを行いながら、 I/O を監視し、利用可能や I/O 完
      了等のイベントが発生するとコールバック(または、シグナル)により通知。
      I/O の監視には、 I/O multiplexing モデルを使用し、環境によって最適なシステ
      ムコール( Linux であれば、 epoll 、 FreeBSD では、 kqueue )を使用。

   libeio
    C 言語で書かれた非同期 I/O ライブラリ
      実装的には、キューとスレッドプールを使い、 I/O を非同期並行処理します。




                                                           17
Node.js のアーキテクチャ
        Java Script                          C / C++




                 Node Standard Library



                        Node Bindings
                     ( socket, http, etc )




       thread pool      event loop              DNS
V8                                                       http parser
          (libeio)        (libev)             (c-ares)




                                                                       18
libev と epoll の比較
   libev と epoll を使って、 echo サーバを実装し、比較
      してみましょう。




                                      19
epoll で echo サーバ
// メイン関数                                                      // クライアントからのイベントを処理する
int main() {                                                  void event_client (int epfd, int client, struct epoll_event ev) {
    int listener, epfd;                                           char buffer[1024];
    struct epoll_event ev;                                        int n = read(client, buffer, sizeof buffer);
    struct epoll_event events[MAX_EVENTS];                        if (n < 0) {
                                                                      perror("read");
    // サーバ起動                                                          epoll_ctl(epfd, EPOLL_CTL_DEL, client, &ev);
    listener = setup_socket();                                        close(client);
                                                                  } else if (n == 0) {
    // epollの初期化                                                      epoll_ctl(epfd, EPOLL_CTL_DEL, client, &ev);
    if ((epfd = epoll_create (MAX_EVENTS)) < 0) {                     close(client);
        die("epoll_create");                                      } else {
    }                                                                 write(client, buffer, n);
    memset(&ev, 0, sizeof ev);                                    }
    ev.events = EPOLLIN;                                      }
    ev.data.fd = listener;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listener, &ev);            // サーバへの接続要求イベントを処理する
                                                              void event_server (int epfd, int listener, struct epoll_event ev) {
    // 無限ループ                                                      struct sockaddr_in client_addr;
    while (1) {                                                   socklen_t client_addr_len = sizeof client_addr;
        int i;
        // 接続があるまで待つ                                              int client = accept(listener, (struct sockaddr *) &client_addr,
        int nfd = epoll_wait(epfd, events, MAX_EVENTS, -1);          &client_addr_len);
        // 接続されているクライアント数分、処理を行う                                  if (client < 0) {
        for (i = 0; i < nfd; i++) {                                   die("accept");
            // 新規接続の場合                                            }
            if (events[i].data.fd == listener) {                  setnonblocking(client);
              event_server(epfd, listener, ev);                   memset(&ev, 0, sizeof ev);
            } else {                                              ev.events = EPOLLIN | EPOLLET;
                event_client(epfd, events[i].data.fd, ev);        ev.data.fd = client;
            }                                                     epoll_ctl(epfd, EPOLL_CTL_ADD, client, &ev);
        }                                                     }
    }

    return 0;
}



epoll() を使う場合、自前で無限ループをつくり、その中で、 epoll_wait() を呼び出し、クライアント
からの接続を待つ必要があります。 epoll_wait() の最後のパラメータにマイナスの値を設定する
と、タイムアウトせずにひたすら待ちます。

                                                                                                                                    20
libev で echo サーバ
// メイン関数                                                     // クライアントからのイベントを処理する
int main() {                                                 void event_client (EV_P_ struct ev_io *w, int revents) {
    struct ev_loop *loop;                                        char buf[RCVBUFSIZE + 1];
    ev_io watcher;                                               size_t n = recv(w->fd, buf, RCVBUFSIZE, 0);
                                                                 if (n < 0) {
    // サーバ起動                                                        perror("recv");
    int listener = setup_socket();                               }
                                                                 if (n <= 0) {
    // イベントループの初期化                                                   close(w->fd);
    loop = ev_default_loop (0);                                      ev_io_stop(EV_A_ w);
    watcher.data = loop;                                             free(w);
                                                                 } else {
    // ev_ioの初期化と開始(サーバへの接続要求を監視)                                    buf[n] = '0';
    ev_io_init(&watcher, event_server, listener, EV_READ);           send(w->fd, buf, n, 0);
    ev_io_start (loop, &watcher);                                }
                                                             }
    // イベントループ開始
    ev_loop(loop, 0);                                        // サーバへの接続要求イベントを処理する
    close(listener);                                         void event_server (EV_P_ struct ev_io *w, int revents) {
    return 0;                                                    struct sockaddr_in client_addr;
}                                                                socklen_t client_addr_len = sizeof(client_addr);
                                                                 struct ev_loop *l;
                                                                 ev_io *client_watcher;

                                                                 int client = accept(w->fd, (struct sockaddr *) &client_addr,
                                                                     &client_addr_len);
                                                                 if (client < 0) {
                                                                 if (EINTR == errno) { return; }
                                                                       die("accept");
                                                                 }
                                                                 setnonblocking(client);
                                                                 client_watcher = calloc(1, sizeof(ev_io));
                                                                 l = w->data;
                                                                 // ev_ioの初期化と開始(クライアントのイベントを監視)
                                                                 ev_io_init(client_watcher, event_client, client, EV_READ);
                                                                 ev_io_start (l, client_watcher);
                                                             }


libev を使用する場合は、無限ループを作成する必要がありません。 ev_loop() で、イベント
ループとよばれる、イベントを監視するループが実行されます。イベントループに関数を設定
して、イベントを監視します。

                                                                                                                                21
int main (void) {
                                          libeio のサンプル                            // EIOスレッドで実行される
    printf ("pipe ()n");                                                         void want_poll (void) {
    if (pipe (respipe)) abort ();                                                     char dummy;
                                                                                      printf ("want_poll ()n");
    printf ("eio_init ()n");                                                         write (respipe [1], &dummy, 1);
    // libeioにwant_poll()とdone_poll()を登録。                                         }
    // libeioがpollして欲しいときに、このwant_poll()が呼び出される。
    if (eio_init (want_poll, done_poll)) abort ();                                // EIOスレッドで実行される
    do {                                                                          void done_poll (void) {
       // eio_open()呼び出し後、メインループを実行する。                                                char dummy;
        eio_open ("eio-test-file", O_RDWR | O_CREAT, 0777, 0, open_cb, "open");       printf ("done_poll ()n");
        event_loop ();                                                                read (respipe [0], &dummy, 1);
                                                                                  }
       // eio_write()呼び出し後、メインループを実行する。
        eio_write (last_fd, "aaaaaaaaaa", 10, 0, 0, res_cb, "write");             // mainスレッドのイベントループ
        event_loop ();                                                            void event_loop (void) {
                                                                                      struct pollfd pfd;
       // eio_read()呼び出し後、メインループを実行する。                                                pfd.fd     = respipe [0];
        eio_read (last_fd, 0, 8, 0, EIO_PRI_DEFAULT, read_cb, "read");                pfd.events = POLLIN;
        event_loop ();
                                                                                      printf ("nentering event loopn");
       // eio_close()呼び出し後、メインループを実行する。                                               while (eio_nreqs ()) {
        eio_close (last_fd, 0, res_cb, "close");                                         // イベントを待つ
        event_loop ();                                                                    poll (&pfd, 1, -1);
    } while (0);                                                                          // mainスレッドでeio_poll()を実行する。
    return 0;                                                                             printf ("eio_poll () = %dn", eio_poll ());
}                                                                                     }
                                                                                      printf ("leaving event loopn");
                                                                                  }
eio-test-file を作成モードで開き、 write(2) と read(2) を実行
し、 close(2) します。                                                                  // mainスレッドで実行される
                                                                                  int res_cb (eio_req *req) {
libeio は、スレッドプールにより並行処理します。 main スレッドと EIO                                            printf ("res_cb(%d|%s) = %dn", req->type,
スレッドで通信を行うために、 eio_init() で、 libeio が poll 通知できる                                         req->data ? req->data : "?", EIO_RESULT (req));
                                                                                      if (req->result < 0) perror(req->errorno);
ようにします。                                                                               return 0;
main スレッドから、 eio_open() 等の eio の I/O 関数が呼ばれると、                                    }
キューにたまります。                                                                        // mainスレッドで実行される
EIO スレッドは、キューから取り出して、システムコールを実行し、結                                                int read_cb (eio_req *req) {
果をキューに返して、 eio_poll() を呼び出すよう main スレッドに通知                                            unsigned char *buf = (unsigned char *)EIO_BUF (req);
                                                                                      printf ("read_cb = %d (%s)n",
します。                                                                                         EIO_RESULT (req),
main スレッドが eio_poll() を呼び出すと、事前に設定していた関数                                                     buf);
                                                                                      return 0;
がコールバックで呼び出されます。コールバックは、 main スレッドで                                               }
行われます。

                                                                                                                                           22
Node.js の処理フロー
   以下のような JavaScript コードを Node.js で実行し
     た場合の処理フローを説明します。
      var path = require('path'),
          fs = require('fs'),
          filepath = path.join(__dirname, 'a.txt'),
      fd = fs.openSync(filepath, 'r');
      fs.read(fd, 1024, 0, 'utf-8', function(err, str,
      bytesRead) {
          console.log(str);
      });


   カレントディレクトリから、 a.txt を読み込みコンソー
     ルに表示するプログラムです。


                                                         23
Node.js の処理フロー




                     24
Node.js の処理フロー
0. Node.js 起動時に eio_init() で node::EIOWantPoll() を libeio に登録する。
  libeio は、 poll して欲しいタイミングになると、 node::EIOWantPoll() を呼び出すようになる。
1. node_file.cc の Read() が実行される。
2. libeio の eio_submit() が実行され、 req_queue に格納される。
  req_queue には、 eio_req 構造体が保存される。
  eio_read() の場合、 {TYPE=EIO_READ, FINISH=After(), DATA->cb=callback()} が設定され
る。 After() は、 node_file.cc の関数で、 libeio で eio_poll() が呼びだされた際に
  main スレッド(イベントループ)で実行される。
  After() の中で、 DATA->cb に設定された callback() が呼ばれる。 callback() には、
  javascript の fs.read() の最後のパラメータとして
 設定された関数が設定される。今回の場合は、
  function(err, str, bytesRead) {
      console.log(str);
   }
3. EIO スレッドで、 eio_execute() が実行され、 read(2) が実行される。
5. read(2) の結果を、 req->result に設定し、 res_queue に格納する。
6. EIO スレッドで、 want_poll() が呼び出される。
7. 先程、 eio_init() で設定した、 node::EIOWantPoll() が呼び出され、 ev_async_send() により、
 イベントループへ通知される。
8. イベントループで、 node::WantPollNotifier() が呼ばれ、 eio_poll() が実行される。
9. イベントループで先程の After() が実行され、 req->result の結果が読み出される。
10. 最後に callback() が実行される。



                                                                          25
インストール
        と
    パッケージ管理




              26
Node.js のインストール( nave )
   nave とは
    Node.js のバージョン管理ソフト( Ruby でいう rvm )
    複数バージョンをひとつの環境で使える
   インストール
    1. Node.js をインストールするディレクトリを作成する。
    $ mkdir nodejs
    2. git コマンドにより、 github から nave を取得します。
    $ git clone http://github.com/isaacs/nave.git
    3. 展開された nave ディレクトリに移動し、「 nave.sh 」を実行します。
    $ cd nave
    $ ./nave.sh install latest
     「 nave.sh 」の install コマンドに対して、「 latest 」オプションを指定しています。
     「 latest 」とは、最新版を意味します。「 latest 」の代わりに
     バージョン番号を指定することもできます。
    4. 「 use 」コマンドを実行し、環境変数 PATH を設定します。
    $ ./nave.sh use latest
     「 latest 」を指定することにより、最新版のインストール先が PATH に設定されます。
     「 install 」コマンドと同様にバージョン番号を指定して、
     特定バージョンの Node.js を動作させることもできます。
    5. バージョン番号が表示されれば、インストール成功です。
    $ node -v
    v0.4.2  
                                                              27
nave のディレクトリ構成
     .
      # nave インストールディレクトリ
     |-- README.md
     |-- installed
      # 各バージョンのバイナリインストールディレクトリ
     | |-- 0.2.0
     (略)
     | |-- 0.4.2
     | | |-- bin
     | | | |-- node
     (略)
     | | | |-- npm -> ./npm@0.3.12
     (略)
     | | |-- include
     | | | `-- node
     | | |       |-- config.h
     | | |       |-- eio.h
     | | |-- lib
     | | | |-- node
     | | | | |-- npm -> ./npm@0.3.12
     |-- src
      # 各バージョンのソースコードディレクトリ
     | |-- 0.2.0
     (略)
     | |-- 0.4.2

                                       28
npm のインストール
   npm とは
    Node.js のパッケージ管理システム( Ruby でいう gem )
   インストール
    $ curl http://npmjs.org/install.sh | sh
     「 nave.sh 」の「 use 」コマンドを実行した状態で、
      npm をインストールすると Node.js と同じように
      npm も nave により管理されるようになります。




                                              29
npm 紹介
               名前                                         概要

Socket.IO                         WebSocket の node.js 実装です。チャット等のリアルタイム通信を作成するのに
http://socket.io/                 使用します。イベントループが本領発揮する領域のパッケージです。
                                  <インストール>
                                  $ npm install socket.io
Express                           Rails ライクなフレームワークです。 MVC の自動生成、ルーティング機能、モデル
http://expressjs.com/             機能等を持っています。
                                  <インストール>
                                  $ npm install express
EJS                               node.js のパッケージの中で、人気のテンプレートエンジンです。
http://embeddedjs.com/            <インストール>
                                  $ npm install ejs

jsdom                             html に対して、 dom 操作が使えるようになるパッケージです。
https://github.com/tmpvar/jsdom   <インストール>
                                  $ npm install jsdom

node-validator                      バリデーションや文字列操作、サニタイズ処理を行うパッケージです。
                                    <インストール>
https://github.com/chriso/node-validator
                                    $ npm install validator
node-oauth                          oauth 認証を行うパッケージです。
                                    <インストール>
https://github.com/ciaranj/node-oauth
                                    $ npm install oauth

node-mysql                         データベースの MySQL へ接続するためのパッケージです。
                                   <インストール>
https://github.com/felixge/node-mysql
                                   $ npm install mysql

                                                                                   30
Node.js 開発環境




                   31
Node.js 開発環境
   Node.js の開発環境を構築します。
   node­dev
    実行中のスクリプトが更新された際に自動的に再起動します。
    スクリプトを修正した時に Node.js を再起動する手間が省けます。
   node­inspector
    ブラウザ上の IDE で、 Node.js のデバッグができます。
    ※Webkit に依存するため、 Google Chrome 等の Webkit に対
     応したブラウザが必要



                                              32
node­dev
   インストール
    npm でインストールします。
    $ npm install node-dev

   使用例
    node コマンドの代わりに、 node­dev コマンドを使用します。
    console.log("hello world");


    $ node-dev hello-world.js
    9 Apr 14:06:39 - [INFO] Started
    hello world                       hello-world.js を修正すると
                                       自動的に最実行される。

    9 Apr 14:06:48 - [INFO] Started
    hello world2

                                                              33
node­inspector
   インストール
    npm でインストールします。
    $ npm install node-inspector
   使用例
    $ node-inspector
    visit http://0.0.0.0:8080/debug?port=5858 to start debugging
     まず node-inspector をコンソールから立ち上げます。

    $ node-dev --debug [ スクリプトファイル ]
     次に別のコンソールで、 node-dev (または、 node コマンド)に対して、
     「 --debug 」オプションを指定して、 Node.js を実行します。

    ブラウザで「 http://127.0.0.1:8080/debug?port=5858 」に接続します。




                                                                   34
デバッグ画面




             35
Cloud9 IDE
   ブラウザ上の IDE です。
   エディタ、シンタックスハイライト、デバッグ実行等ができます。
   インストール
    $ npm install cloud9
   実行(実行したいディレクトリで)
    $cloud9




                                     36
Cloud9 IDE  画面イメージ




                         37
Node.js (実践編)
    リアルタイム通信




                    38
Ajax vs Comet
   Ajax
    Ajax を使用して、サーバへポーリング ( 一定間隔でサーバをチェックする )
                   ◇ デメリット
                  ポーリングの間隔分の遅延が発生する
                  データ変更があるなしに関わらずチェックを行うため、 CPU やメモリを必
                    要以上に使用してしまう
                  ポーリング間隔が短すぎればネットワーク帯域やリソースを消費しすぎる
   Comet
    HTTP を使った(無理やり)プッシュ通信技術。クライアントからのリクエス
      トに対してすぐに応答せずに、サーバ上でイベントが発生したときにレス
      ポンスを返す。
                   ◇ デメリット
                  クライアントへ 2 倍のリソースが必要。
                   (ブラウザからサーバへの通常のリクエストは別 HTTP コネクショ
                     ンでやり取りするため)
                  ブラウザによって挙動が変わる場合も。。
                                                      39
WebSocket

    そこで、 WebSocket
   クライアントとサーバー間で双方向通信を実現す
     るための仕組み。
   接続の確立までは HTTP を使用し、その後は
     WebSocket 独自のプロトコルに切り替える。
   ※ 但し、現在、仕様策定中。。




                                 40
Node.js でそれぞれのサンプルを実装




                        41
チープチャット ­Ajax Poll 版 ­
HTML
<html>
<head>
      <title>Ajax Poll Chat</title>
      <script type="text/javascript" src="http://localhost/js/jquery-1.4.4.min.js"></script>
      <script type="text/javascript" src="http://localhost/js/jquery-ui-1.8.2.custom.min.js"></script>
      <script type="text/javascript" src="http://localhost/js/poll.js"></script>

</head>
<body>
<h1>Ajax Poll Chat</h1>

<!-- ログインフォーム -->
<form id="login_form">
      <span>Enter username:</span>
      <input id="user_name" type="text" size="10" value="" />
      <input id="login_button" type="button" value="Login" />
</form>

<!-- メッセージ書き込みフォーム -->
<form id="write_form">
      <span id="write_username"></span>
      <input id="write_message" type="text" size="40" value="" />
      <input id="write_button" type="button" value="Write" />
</form>

<!-- ログインアウトフォーム -->
<form id="logout_form">
      <input id="logout_button" type="button" value="Logout" />
</form>

<h2>Chat Log</h2>
<span id ="result"></span>

</body>
</html>
                                                                                                         42
サーバ側 JavaScript
var http = require('http'),
      fs = require('fs'),
      url = require('url'),
      querystring = require('querystring');

// メッセージ
var messages = [];

// http サーバの作成
http.createServer(function(req, res) {
        // index.html を表示
        if (req.url == "/") {
               fs.readFile(__dirname + '/index.html', function(err, content) {
                      res.writeHead(200, {'Content-Type':'text/html; charset=utf-8'});
                      res.end(content);
               });
        // メッセージを返す
        } else if (req.url == "/read_message.json") {
               res.writeHead(200, {'Content-Type':'application/json; charset=utf-8'});
               res.end(JSON.stringify(messages));
        // メッセージの書き込み
        } else {
               // URL パラメータの取得
               var param = querystring.parse(url.parse(req.url).query);
               // メッセージの保存
               messages.push(param);
               console.log(param);
               // メッセージを返す
               res.writeHead(200, {'Content-Type':'application/json; charset=utf-8'});
               res.end(JSON.stringify(messages));
        }
}).listen(8192, '127.0.0.1');
console.log('http://127.0.0.1:8192/');

                                                                                         43
クライアント側 JavaScript
// インターバル                                                                                // ログインボタン
var watch = null;                                                                        function set_login_button() {
                                                                                                $("#login_button").click(function(){
// 初期化処理                                                                                               read();
function init() {                                                                                      watch = setInterval(read, 5000);
       $("#write_form").hide();                                                                        $("#write_form").show();
       $("#logout_form").hide();                                                                       $("#logout_form").show();
       $("#user_name").val("");                                                                 });
}                                                                                        }

// メッセージ出力                                                                               // ログアウトボタン
function show(data) {                                                                    function set_logout_button() {
       var result = "";                                                                         $("#logout_button").click(function(){
       $.each(data, function() {                                                                       clearInterval(watch);
              if (this.write_message != null) {                                                        init();
                       result += this.user_name + ":" + this.write_message + "<br />";          });
              }                                                                          }
       });
       $("#result").html(result);                                                        $(document).ready(function() {
}                                                                                              init();
                                                                                               set_login_button();
// メッセージの読み込み                                                                                  set_logout_button();
function read() {                                                                              set_write_button();
       $.getJSON("read_message.json", null, function(data, status){                      });
             show(data);
       });
}

// メッセージ書き込み
function set_write_button() {
       $("#write_button").click(function(){
             var data = {};
             data.user_name = $("#user_name").val();
             data.write_message = $("#write_message").val();
             $.getJSON("write_message.json", data, function(data, status){
                    read();
             });
       });
}
                                                                                                                                        44
チープチャット ­Comet 版 ­
サーバ側 JavaScript
var http = require('http'),                                     // http サーバの作成
       fs = require('fs'),                                      http.createServer(function(req, res) {
       url = require('url'),                                            // index.html を表示
       querystring = require('querystring');                            if (req.url == "/") {
                                                                                fs.readFile(__dirname + '/index.html', function(err, content) {
// メッセージ                                                                                console.log("connections.length:" + connections.length);
var messages = [];                                                                      res.writeHead(200, {'Content-Type':'text/html;
// コネクション                                                                                      charset=utf-8'});
var connections = [];                                                                   res.end(content);
                                                                                });
// 全クライアントに通知                                                           // メッセージ取得
function notify() {                                                     } else if (req.url == "/read_message.json") {
       if (connections.length) {                                                // コネクションに入れて Long Poll する(レスポンスを返さない)
              var c = null;                                                     connections.push(res);
              while ((c = connections.shift()) != null) {                       console.log("connections.length:" + connections.length);
                     // メッセージを返す                                        } else {
                     c.writeHead(200,                                           // URL パラメータの取得
                     {'Content-Type':'application/json;                         var param = querystring.parse(url.parse(req.url).query);
                     charset=utf-8'});                                          // メッセージの保存
                     c.end(JSON.stringify(messages));                           messages.push(param);
              }                                                                 console.log(param);
       }                                                                        console.log("connections.length:" + connections.length);
       // 60 秒待って、全クライアントにメッセージを返す                                              // 全クライアントに通知
       setTimeout(notify, 60000);                                               notify();
}                                                                               // メッセージを返す
                                                                                res.writeHead(200,
// 60 秒待って、全クライアントにメッセージを返す                                                     {'Content-Type':'application/json; charset=utf-8'});
setTimeout(notify, 60000);                                                      res.end(JSON.stringify(messages));
                                                                        }
                                                                }).listen(8192, '127.0.0.1');
                                                                console.log('http://127.0.0.1:8192/');




                                                                                                                                               45
クライアント側 JavaScript
// 初期化処理                                                                                 // ログインボタン
function init() {                                                                        function set_login_button() {
       $("#write_form").hide();                                                                 $("#login_button").click(function(){
       $("#logout_form").hide();                                                                       read();
       $("#user_name").val("");                                                                        $("#write_form").show();
}                                                                                                      $("#logout_form").show();
                                                                                                });
// メッセージ出力                                                                               }
function show(data) {
       var result = "";                                                                  // ログアウトボタン
       $.each(data, function() {                                                         function set_logout_button() {
              if (this.write_message != null) {                                                 $("#logout_button").click(function(){
                       result += this.user_name + ":" + this.write_message + "<br />";                 init();
              }                                                                                 });
       });                                                                               }
       $("#result").html(result);
}                                                                                        $(document).ready(function() {
                                                                                               init();
// メッセージの読み込み                                                                                  set_login_button();
function read() {                                                                              set_logout_button();
       // Comet 通知用のコネクション                                                                     set_write_button();
       $.getJSON("read_message.json", null, function(data, status){                      });
             show(data);
             // 再度コネクションをはる
             read();
       });
}

// メッセージ書き込み
function set_write_button() {
       $("#write_button").click(function(){
             var data = {};
             data.user_name = $("#user_name").val();
             data.write_message = $("#write_message").val();
             $.getJSON("write_message.json", data, function(data, status){
                    show(data);
             });
       });
}
                                                                                                                                        46
チープチャット ­WebSocket 版 ­
                  var http = require('http'),
サーバ側 JavaScript
                        fs = require('fs'),
                        ws = require('websocket-server');

                  // WebSocket サーバの作成
                  var server = ws.createServer();

                  // 新規接続
                  server.addListener("connection", function(connection) {
                       console.log("connect");
                       // メッセージ受信
                       connection.addListener("message", function(message){
                              console.log(message);
                              // 全クライアントにメッセージを送る
                              server.broadcast(message);
                       });
                  });
                  // クローズ
                  server.addListener("close", function(connection) {
                       console.log("close");
                  });
                  // WebSocket サーバ待ち受け
                  server.listen(8000);

                  // http サーバの作成
                  http.createServer(function(req, res) {
                          // index.html を表示
                          fs.readFile(__dirname + '/index.html', function(err, content) {
                                 res.writeHead(200, {'Content-Type':'text/html; charset=utf-8'});
                                 res.end(content);
                          });
                  }).listen(8192, '127.0.0.1');
                  console.log('http://127.0.0.1:8192/');
                                                                                                    47
クライアント側 JavaScript
var ws = new WebSocket("ws://localhost:8000");        // ログインボタン
                                                      function set_login_button() {
// メッセージ受信時                                                 $("#login_button").click(function(){
ws.onmessage = function(message) {                                $("#write_form").show();
     var data = $.parseJSON(message.data);                        $("#logout_form").show();
     show(data);                                            });
}                                                     }

// 初期化処理                                              // ログアウトボタン
function init() {                                     function set_logout_button() {
      $("#write_form").hide();                              $("#logout_button").click(function(){
      $("#logout_form").hide();                                   init();
      $("#user_name").val("");                              });
}                                                     }

// メッセージ出力                                            $(document).ready(function() {
function show(data) {                                      init();
      var result = data.user_name + ":" +                  set_login_button();
            data.write_message + "<br />";                 set_logout_button();
      $("#result").append(result);                         set_write_button();
}                                                     });

// メッセージ書き込み
function set_write_button() {
      $("#write_button").click(function(){
            var data = {};
            data.user_name = $("#user_name").val();
            data.write_message =
                  $("#write_message").val();
            ws.send($.toJSON(data));
      });
}
                                                                                                    48
参考資料
   私のブログ「 shutdown ­h now 」 http://d.hatena.ne.jp/forest1040/
   developerWorks 「 Boost application performance using asynchronous I/O 」 
       http://www.ibm.com/developerworks/linux/library/l­async/
   「同期 I/O 」と「非同期 I/O 」の定義、とか  http://d.hatena.ne.jp/hirose31/20070815
   非同期 I/O  概説  http://www.slideshare.net/hirose31/aio
   epoll  を使った echo  サーバ  http://d.hatena.ne.jp/odz/20070507/1178558340
   libev で echo サーバを作る  http://d.hatena.ne.jp/winebarrel/20080309/p2
   C10K 問題  http://www.hyuki.com/yukiwiki/wiki.cgi?TheC10kProblem
   node.js  のソースぐらい読んでおきたい!  http://d.hatena.ne.jp/edvakf/20101207/1291556433
   C++  で node.js  ライブラリを作る・その 2   http://nodejs.g.hatena.ne.jp/edvakf/20101214/1292287495
   IT Pro Comet  プッシュ型の Web アプリケーションを作る 
        http://itpro.nikkeibp.co.jp/article/COLUMN/20080220/294242/
   node.js と WebSocket の利用シーン  http://bizria.jp/technical/nodejs­webssocket.html
   @IT Node.js でサーバサイド JavaScript 開発入門」 
       http://www.atmarkit.co.jp/fwcr/index/index_nodejs.html



                                                                                              49
ご清聴ありがとうございました。
    Node.js は、 JavaScript を使って、比較的手軽
     に非同期 I/O のシステムを実装できます。
    ◇ 本日のふりかえり
   Node.js とは
   非同期 I/O とイベントループ
   Node.js のアーキテクチャ
   インストール
   デバッグ環境
   Node.js を使ったリアルタイム通信

                                   50

More Related Content

Node.js入門

  • 1. Node.js 入門 2011 年 4 月 16 日 森 俊夫 @ 徳島 [email protected] http://d.hatena.ne.jp/forest1040/     1
  • 2. 自己紹介  id:forest1040  です。  徳島で、フリーランスをやってます。  Web 系エンジニアです。最近は、 Java EE(JBoss  Seam) と Ruby をよく使っています。  1年前に息子が生まれ、イクメン中です。  Node.js on Android をやってます。     2
  • 3. アジェンダ  基礎編  Node.js とは  非同期 I/O とイベントループ  Node.js のアーキテクチャ  実践編  インストール  デバッグ環境  Node.js を使ったリアルタイム通信     3
  • 4. Node.js とは  サーバサイド JavaScript  Google の V8 エンジン搭載  シングルスレッド非同期 I/O 環境  イベントループモデル     4
  • 5. シングルスレッド非同期 I/O 環境 と イベントループ     5
  • 6. 頭の体操(並行処理) 突然ですが、クイズです。  シングルスレッドで並行処理を行うには?  マルチスレッドとシングルスレッドの 並行処理の違いは?     6
  • 7. 並行処理  マルチスレッド( multithread )による並行処理 呼び元と並行に処理が行われる。  シングルスレッドでのコールバック( callback )によ る並行処理 呼び元がプロセッサを使用していないときに処理 が行われる。     7
  • 8. 非同期と同期、ノンブロッキングとブロッキング  さらにクイズです。 非同期とノンブロックの違いがわかります か?     8
  • 9. 非同期とノンブロッキング  実は、非同期( Asynchronous )とノンブロッキング ( non­blocking )は同じ意味としてよく使われて いる。  日本語に惑わされると負け。     9
  • 10. 非同期(ノンブロッキング)とは 非同期呼び出し( Asynchronous Call )と同期呼び 出し( Synchronous Call )の違い  同期呼び出し( Synchronous Call ) 通常メソッドを呼び出すとメソッド内の処理が完了 するまで、呼び出し元には戻ってこない。このよう なメソッド呼び出しのこと。  非同期呼び出し( Asynchronous Call ) メソッドを呼び出した瞬間に呼び出し元に処理が 戻ってくるような呼び出しのこと。非同期で呼び 出されたメソッドは、環境によって処理されるタイ ミングが変わる。 まさしくノンブロッキング     10
  • 11. ノンブロッキング I/O とは ブロッキング I/O ( blocking I/O )とノンブロッキン グ I/O ( non­blocking I/O )の違い  ブロッキング I/O ( blocking I/O ) データ処理が完了するまで待たされること。  ノンブロッキング I/O ( non­blocking I/O ) データ処理の完了を待たされずに、他の処理を行 えること。     11
  • 12. I/O モデル W. Richard Stevens の 「 UNIX ネットワークプログラミング 第 2 版 Vol.1 」  Synchronous I/O Operation ( 同期 I/O 操作 )  blocking I/O ( ブロッキング I/O)  non­blocking I/O ( 非ブロッキング I/O)  I/O multiplexing (I/O 多重化 )  signal driven I/O ( シグナル駆動 I/O)  Asynchronous I/O Operation ( 非同期 I/O 操作 )  Asynchronous I/O ( 非同期 I/O)     12
  • 13. Boost application performance using  asynchronous I/O 引用: Boost application performance using asynchronous I/O http://www­128.ibm.com/developerworks/linux/library/l­async/     13
  • 14. Boost application performance using  asynchronous I/O この図では、 I/O multiplexing が Asynchronous に分類されている。 引用: Boost application performance using asynchronous I/O http://www­128.ibm.com/developerworks/linux/library/l­async/     14
  • 15. C10K 問題  ハードウェアの性能上は問題がなくても、あまりにもクライアントの数 が多くなるとサーバがパンクする問題のこと。 ◇ 解決方法 1. 各スレッドが複数のクライアントを受け付ける。 そしてノンブロッキン グ I/O  と レベル・トリガ型の完了通知 (level­triggered readiness  notification) を利用する。 2. 各スレッドが複数のクライアントを受け付ける。 そしてノンブロッキン グ I/O  と 変更型の完了通知 (readiness change notification) を利 用する。 3. 各スレッドが複数のクライアントを受けつける。 そして非同期 I/O  を 使う。 4. 各スレッドが一つのクライアントを受けつける。 そしてブロッキング I/O  を使う。 5. サーバのコードをカーネルに組込む。     15
  • 16. マルチスレッド vs  ノンブロッキング  apache  マルチスレッドモデル (C10K の解決案 No.4)  nginx  ノンブロッキング( C10K の解決案 No.1 ) 1秒あたりの処理リクエスト数 メモリ使用量 nginx  は、最高で秒間 10,000 リクエストを処理する。同時接続数を 増やしても、リクエスト処理数は少々減る程度  apache の場合、同時接続数を増やすと著しくリクエスト処理数が減 る。しかも、メモリ使用量が同時接続数に比例して増える。 つまり、同時接続数が多い場合は、ノンブロッキングが有利!! 参照: http://blog.webfaction.com/a­little­holiday­present     16
  • 17. Node.js の場合  「 3.  各スレッドが複数のクライアントを受けつける。 そして 非同期 I/O  を使う」を採用  「 libev 」と「 libeio 」を使用して、非同期 I/O 環境を実装  libev C 言語で書かれたイベントループライブラリ イベントループとは、無限ループを行いながら、 I/O を監視し、利用可能や I/O 完 了等のイベントが発生するとコールバック(または、シグナル)により通知。 I/O の監視には、 I/O multiplexing モデルを使用し、環境によって最適なシステ ムコール( Linux であれば、 epoll 、 FreeBSD では、 kqueue )を使用。  libeio C 言語で書かれた非同期 I/O ライブラリ 実装的には、キューとスレッドプールを使い、 I/O を非同期並行処理します。     17
  • 18. Node.js のアーキテクチャ Java Script C / C++ Node Standard Library Node Bindings ( socket, http, etc ) thread pool event loop DNS V8 http parser (libeio) (libev) (c-ares)     18
  • 19. libev と epoll の比較  libev と epoll を使って、 echo サーバを実装し、比較 してみましょう。     19
  • 20. epoll で echo サーバ // メイン関数 // クライアントからのイベントを処理する int main() { void event_client (int epfd, int client, struct epoll_event ev) { int listener, epfd; char buffer[1024]; struct epoll_event ev; int n = read(client, buffer, sizeof buffer); struct epoll_event events[MAX_EVENTS]; if (n < 0) { perror("read"); // サーバ起動 epoll_ctl(epfd, EPOLL_CTL_DEL, client, &ev); listener = setup_socket(); close(client); } else if (n == 0) { // epollの初期化 epoll_ctl(epfd, EPOLL_CTL_DEL, client, &ev); if ((epfd = epoll_create (MAX_EVENTS)) < 0) { close(client); die("epoll_create"); } else { } write(client, buffer, n); memset(&ev, 0, sizeof ev); } ev.events = EPOLLIN; } ev.data.fd = listener; epoll_ctl(epfd, EPOLL_CTL_ADD, listener, &ev); // サーバへの接続要求イベントを処理する void event_server (int epfd, int listener, struct epoll_event ev) { // 無限ループ struct sockaddr_in client_addr; while (1) { socklen_t client_addr_len = sizeof client_addr; int i; // 接続があるまで待つ int client = accept(listener, (struct sockaddr *) &client_addr, int nfd = epoll_wait(epfd, events, MAX_EVENTS, -1); &client_addr_len); // 接続されているクライアント数分、処理を行う if (client < 0) { for (i = 0; i < nfd; i++) { die("accept"); // 新規接続の場合 } if (events[i].data.fd == listener) { setnonblocking(client); event_server(epfd, listener, ev); memset(&ev, 0, sizeof ev); } else { ev.events = EPOLLIN | EPOLLET; event_client(epfd, events[i].data.fd, ev); ev.data.fd = client; } epoll_ctl(epfd, EPOLL_CTL_ADD, client, &ev); } } } return 0; } epoll() を使う場合、自前で無限ループをつくり、その中で、 epoll_wait() を呼び出し、クライアント からの接続を待つ必要があります。 epoll_wait() の最後のパラメータにマイナスの値を設定する と、タイムアウトせずにひたすら待ちます。     20
  • 21. libev で echo サーバ // メイン関数 // クライアントからのイベントを処理する int main() { void event_client (EV_P_ struct ev_io *w, int revents) { struct ev_loop *loop; char buf[RCVBUFSIZE + 1]; ev_io watcher; size_t n = recv(w->fd, buf, RCVBUFSIZE, 0); if (n < 0) { // サーバ起動 perror("recv"); int listener = setup_socket(); } if (n <= 0) { // イベントループの初期化 close(w->fd); loop = ev_default_loop (0); ev_io_stop(EV_A_ w); watcher.data = loop; free(w); } else { // ev_ioの初期化と開始(サーバへの接続要求を監視) buf[n] = '0'; ev_io_init(&watcher, event_server, listener, EV_READ); send(w->fd, buf, n, 0); ev_io_start (loop, &watcher); } } // イベントループ開始 ev_loop(loop, 0); // サーバへの接続要求イベントを処理する close(listener); void event_server (EV_P_ struct ev_io *w, int revents) { return 0; struct sockaddr_in client_addr; } socklen_t client_addr_len = sizeof(client_addr); struct ev_loop *l; ev_io *client_watcher; int client = accept(w->fd, (struct sockaddr *) &client_addr, &client_addr_len); if (client < 0) { if (EINTR == errno) { return; } die("accept"); } setnonblocking(client); client_watcher = calloc(1, sizeof(ev_io)); l = w->data; // ev_ioの初期化と開始(クライアントのイベントを監視) ev_io_init(client_watcher, event_client, client, EV_READ); ev_io_start (l, client_watcher); } libev を使用する場合は、無限ループを作成する必要がありません。 ev_loop() で、イベント ループとよばれる、イベントを監視するループが実行されます。イベントループに関数を設定 して、イベントを監視します。     21
  • 22. int main (void) { libeio のサンプル // EIOスレッドで実行される printf ("pipe ()n"); void want_poll (void) { if (pipe (respipe)) abort (); char dummy; printf ("want_poll ()n"); printf ("eio_init ()n"); write (respipe [1], &dummy, 1); // libeioにwant_poll()とdone_poll()を登録。 } // libeioがpollして欲しいときに、このwant_poll()が呼び出される。 if (eio_init (want_poll, done_poll)) abort (); // EIOスレッドで実行される do { void done_poll (void) { // eio_open()呼び出し後、メインループを実行する。 char dummy; eio_open ("eio-test-file", O_RDWR | O_CREAT, 0777, 0, open_cb, "open"); printf ("done_poll ()n"); event_loop (); read (respipe [0], &dummy, 1); } // eio_write()呼び出し後、メインループを実行する。 eio_write (last_fd, "aaaaaaaaaa", 10, 0, 0, res_cb, "write"); // mainスレッドのイベントループ event_loop (); void event_loop (void) { struct pollfd pfd; // eio_read()呼び出し後、メインループを実行する。 pfd.fd = respipe [0]; eio_read (last_fd, 0, 8, 0, EIO_PRI_DEFAULT, read_cb, "read"); pfd.events = POLLIN; event_loop (); printf ("nentering event loopn"); // eio_close()呼び出し後、メインループを実行する。 while (eio_nreqs ()) { eio_close (last_fd, 0, res_cb, "close"); // イベントを待つ event_loop (); poll (&pfd, 1, -1); } while (0); // mainスレッドでeio_poll()を実行する。 return 0; printf ("eio_poll () = %dn", eio_poll ()); } } printf ("leaving event loopn"); } eio-test-file を作成モードで開き、 write(2) と read(2) を実行 し、 close(2) します。 // mainスレッドで実行される int res_cb (eio_req *req) { libeio は、スレッドプールにより並行処理します。 main スレッドと EIO printf ("res_cb(%d|%s) = %dn", req->type, スレッドで通信を行うために、 eio_init() で、 libeio が poll 通知できる req->data ? req->data : "?", EIO_RESULT (req)); if (req->result < 0) perror(req->errorno); ようにします。 return 0; main スレッドから、 eio_open() 等の eio の I/O 関数が呼ばれると、 } キューにたまります。 // mainスレッドで実行される EIO スレッドは、キューから取り出して、システムコールを実行し、結 int read_cb (eio_req *req) { 果をキューに返して、 eio_poll() を呼び出すよう main スレッドに通知 unsigned char *buf = (unsigned char *)EIO_BUF (req); printf ("read_cb = %d (%s)n", します。 EIO_RESULT (req), main スレッドが eio_poll() を呼び出すと、事前に設定していた関数 buf); return 0; がコールバックで呼び出されます。コールバックは、 main スレッドで } 行われます。     22
  • 23. Node.js の処理フロー  以下のような JavaScript コードを Node.js で実行し た場合の処理フローを説明します。 var path = require('path'), fs = require('fs'), filepath = path.join(__dirname, 'a.txt'), fd = fs.openSync(filepath, 'r'); fs.read(fd, 1024, 0, 'utf-8', function(err, str, bytesRead) { console.log(str); });  カレントディレクトリから、 a.txt を読み込みコンソー ルに表示するプログラムです。     23
  • 25. Node.js の処理フロー 0. Node.js 起動時に eio_init() で node::EIOWantPoll() を libeio に登録する。   libeio は、 poll して欲しいタイミングになると、 node::EIOWantPoll() を呼び出すようになる。 1. node_file.cc の Read() が実行される。 2. libeio の eio_submit() が実行され、 req_queue に格納される。   req_queue には、 eio_req 構造体が保存される。   eio_read() の場合、 {TYPE=EIO_READ, FINISH=After(), DATA->cb=callback()} が設定され る。 After() は、 node_file.cc の関数で、 libeio で eio_poll() が呼びだされた際に   main スレッド(イベントループ)で実行される。   After() の中で、 DATA->cb に設定された callback() が呼ばれる。 callback() には、   javascript の fs.read() の最後のパラメータとして  設定された関数が設定される。今回の場合は、   function(err, str, bytesRead) { console.log(str); } 3. EIO スレッドで、 eio_execute() が実行され、 read(2) が実行される。 5. read(2) の結果を、 req->result に設定し、 res_queue に格納する。 6. EIO スレッドで、 want_poll() が呼び出される。 7. 先程、 eio_init() で設定した、 node::EIOWantPoll() が呼び出され、 ev_async_send() により、  イベントループへ通知される。 8. イベントループで、 node::WantPollNotifier() が呼ばれ、 eio_poll() が実行される。 9. イベントループで先程の After() が実行され、 req->result の結果が読み出される。 10. 最後に callback() が実行される。     25
  • 26. インストール と パッケージ管理     26
  • 27. Node.js のインストール( nave )  nave とは Node.js のバージョン管理ソフト( Ruby でいう rvm ) 複数バージョンをひとつの環境で使える  インストール 1. Node.js をインストールするディレクトリを作成する。 $ mkdir nodejs 2. git コマンドにより、 github から nave を取得します。 $ git clone http://github.com/isaacs/nave.git 3. 展開された nave ディレクトリに移動し、「 nave.sh 」を実行します。 $ cd nave $ ./nave.sh install latest  「 nave.sh 」の install コマンドに対して、「 latest 」オプションを指定しています。  「 latest 」とは、最新版を意味します。「 latest 」の代わりに  バージョン番号を指定することもできます。 4. 「 use 」コマンドを実行し、環境変数 PATH を設定します。 $ ./nave.sh use latest  「 latest 」を指定することにより、最新版のインストール先が PATH に設定されます。  「 install 」コマンドと同様にバージョン番号を指定して、  特定バージョンの Node.js を動作させることもできます。 5. バージョン番号が表示されれば、インストール成功です。 $ node -v v0.4.2       27
  • 28. nave のディレクトリ構成 . # nave インストールディレクトリ |-- README.md |-- installed # 各バージョンのバイナリインストールディレクトリ | |-- 0.2.0 (略) | |-- 0.4.2 | | |-- bin | | | |-- node (略) | | | |-- npm -> ./[email protected] (略) | | |-- include | | | `-- node | | | |-- config.h | | | |-- eio.h | | |-- lib | | | |-- node | | | | |-- npm -> ./[email protected] |-- src # 各バージョンのソースコードディレクトリ | |-- 0.2.0 (略) | |-- 0.4.2     28
  • 29. npm のインストール  npm とは Node.js のパッケージ管理システム( Ruby でいう gem )  インストール $ curl http://npmjs.org/install.sh | sh  「 nave.sh 」の「 use 」コマンドを実行した状態で、   npm をインストールすると Node.js と同じように   npm も nave により管理されるようになります。     29
  • 30. npm 紹介 名前 概要 Socket.IO WebSocket の node.js 実装です。チャット等のリアルタイム通信を作成するのに http://socket.io/ 使用します。イベントループが本領発揮する領域のパッケージです。 <インストール> $ npm install socket.io Express Rails ライクなフレームワークです。 MVC の自動生成、ルーティング機能、モデル http://expressjs.com/ 機能等を持っています。 <インストール> $ npm install express EJS node.js のパッケージの中で、人気のテンプレートエンジンです。 http://embeddedjs.com/ <インストール> $ npm install ejs jsdom html に対して、 dom 操作が使えるようになるパッケージです。 https://github.com/tmpvar/jsdom <インストール> $ npm install jsdom node-validator バリデーションや文字列操作、サニタイズ処理を行うパッケージです。 <インストール> https://github.com/chriso/node-validator $ npm install validator node-oauth oauth 認証を行うパッケージです。 <インストール> https://github.com/ciaranj/node-oauth $ npm install oauth node-mysql データベースの MySQL へ接続するためのパッケージです。 <インストール> https://github.com/felixge/node-mysql $ npm install mysql     30
  • 32. Node.js 開発環境  Node.js の開発環境を構築します。  node­dev 実行中のスクリプトが更新された際に自動的に再起動します。 スクリプトを修正した時に Node.js を再起動する手間が省けます。  node­inspector ブラウザ上の IDE で、 Node.js のデバッグができます。 ※Webkit に依存するため、 Google Chrome 等の Webkit に対 応したブラウザが必要     32
  • 33. node­dev  インストール npm でインストールします。 $ npm install node-dev  使用例 node コマンドの代わりに、 node­dev コマンドを使用します。 console.log("hello world"); $ node-dev hello-world.js 9 Apr 14:06:39 - [INFO] Started hello world hello-world.js を修正すると 自動的に最実行される。 9 Apr 14:06:48 - [INFO] Started hello world2     33
  • 34. node­inspector  インストール npm でインストールします。 $ npm install node-inspector  使用例 $ node-inspector visit http://0.0.0.0:8080/debug?port=5858 to start debugging  まず node-inspector をコンソールから立ち上げます。 $ node-dev --debug [ スクリプトファイル ]  次に別のコンソールで、 node-dev (または、 node コマンド)に対して、  「 --debug 」オプションを指定して、 Node.js を実行します。 ブラウザで「 http://127.0.0.1:8080/debug?port=5858 」に接続します。     34
  • 36. Cloud9 IDE  ブラウザ上の IDE です。  エディタ、シンタックスハイライト、デバッグ実行等ができます。  インストール $ npm install cloud9  実行(実行したいディレクトリで) $cloud9     36
  • 38. Node.js (実践編) リアルタイム通信     38
  • 39. Ajax vs Comet  Ajax Ajax を使用して、サーバへポーリング ( 一定間隔でサーバをチェックする ) ◇ デメリット  ポーリングの間隔分の遅延が発生する  データ変更があるなしに関わらずチェックを行うため、 CPU やメモリを必 要以上に使用してしまう  ポーリング間隔が短すぎればネットワーク帯域やリソースを消費しすぎる  Comet HTTP を使った(無理やり)プッシュ通信技術。クライアントからのリクエス トに対してすぐに応答せずに、サーバ上でイベントが発生したときにレス ポンスを返す。 ◇ デメリット  クライアントへ 2 倍のリソースが必要。 (ブラウザからサーバへの通常のリクエストは別 HTTP コネクショ ンでやり取りするため)  ブラウザによって挙動が変わる場合も。。     39
  • 40. WebSocket そこで、 WebSocket  クライアントとサーバー間で双方向通信を実現す るための仕組み。  接続の確立までは HTTP を使用し、その後は WebSocket 独自のプロトコルに切り替える。  ※ 但し、現在、仕様策定中。。     40
  • 42. チープチャット ­Ajax Poll 版 ­ HTML <html> <head> <title>Ajax Poll Chat</title> <script type="text/javascript" src="http://localhost/js/jquery-1.4.4.min.js"></script> <script type="text/javascript" src="http://localhost/js/jquery-ui-1.8.2.custom.min.js"></script> <script type="text/javascript" src="http://localhost/js/poll.js"></script> </head> <body> <h1>Ajax Poll Chat</h1> <!-- ログインフォーム --> <form id="login_form"> <span>Enter username:</span> <input id="user_name" type="text" size="10" value="" /> <input id="login_button" type="button" value="Login" /> </form> <!-- メッセージ書き込みフォーム --> <form id="write_form"> <span id="write_username"></span> <input id="write_message" type="text" size="40" value="" /> <input id="write_button" type="button" value="Write" /> </form> <!-- ログインアウトフォーム --> <form id="logout_form"> <input id="logout_button" type="button" value="Logout" /> </form> <h2>Chat Log</h2> <span id ="result"></span> </body> </html>     42
  • 43. サーバ側 JavaScript var http = require('http'), fs = require('fs'), url = require('url'), querystring = require('querystring'); // メッセージ var messages = []; // http サーバの作成 http.createServer(function(req, res) { // index.html を表示 if (req.url == "/") { fs.readFile(__dirname + '/index.html', function(err, content) { res.writeHead(200, {'Content-Type':'text/html; charset=utf-8'}); res.end(content); }); // メッセージを返す } else if (req.url == "/read_message.json") { res.writeHead(200, {'Content-Type':'application/json; charset=utf-8'}); res.end(JSON.stringify(messages)); // メッセージの書き込み } else { // URL パラメータの取得 var param = querystring.parse(url.parse(req.url).query); // メッセージの保存 messages.push(param); console.log(param); // メッセージを返す res.writeHead(200, {'Content-Type':'application/json; charset=utf-8'}); res.end(JSON.stringify(messages)); } }).listen(8192, '127.0.0.1'); console.log('http://127.0.0.1:8192/');     43
  • 44. クライアント側 JavaScript // インターバル // ログインボタン var watch = null; function set_login_button() { $("#login_button").click(function(){ // 初期化処理 read(); function init() { watch = setInterval(read, 5000); $("#write_form").hide(); $("#write_form").show(); $("#logout_form").hide(); $("#logout_form").show(); $("#user_name").val(""); }); } } // メッセージ出力 // ログアウトボタン function show(data) { function set_logout_button() { var result = ""; $("#logout_button").click(function(){ $.each(data, function() { clearInterval(watch); if (this.write_message != null) { init(); result += this.user_name + ":" + this.write_message + "<br />"; }); } } }); $("#result").html(result); $(document).ready(function() { } init(); set_login_button(); // メッセージの読み込み set_logout_button(); function read() { set_write_button(); $.getJSON("read_message.json", null, function(data, status){ }); show(data); }); } // メッセージ書き込み function set_write_button() { $("#write_button").click(function(){ var data = {}; data.user_name = $("#user_name").val(); data.write_message = $("#write_message").val(); $.getJSON("write_message.json", data, function(data, status){ read(); }); }); }     44
  • 45. チープチャット ­Comet 版 ­ サーバ側 JavaScript var http = require('http'), // http サーバの作成 fs = require('fs'), http.createServer(function(req, res) { url = require('url'), // index.html を表示 querystring = require('querystring'); if (req.url == "/") { fs.readFile(__dirname + '/index.html', function(err, content) { // メッセージ console.log("connections.length:" + connections.length); var messages = []; res.writeHead(200, {'Content-Type':'text/html; // コネクション charset=utf-8'}); var connections = []; res.end(content); }); // 全クライアントに通知 // メッセージ取得 function notify() { } else if (req.url == "/read_message.json") { if (connections.length) { // コネクションに入れて Long Poll する(レスポンスを返さない) var c = null; connections.push(res); while ((c = connections.shift()) != null) { console.log("connections.length:" + connections.length); // メッセージを返す } else { c.writeHead(200, // URL パラメータの取得 {'Content-Type':'application/json; var param = querystring.parse(url.parse(req.url).query); charset=utf-8'}); // メッセージの保存 c.end(JSON.stringify(messages)); messages.push(param); } console.log(param); } console.log("connections.length:" + connections.length); // 60 秒待って、全クライアントにメッセージを返す // 全クライアントに通知 setTimeout(notify, 60000); notify(); } // メッセージを返す res.writeHead(200, // 60 秒待って、全クライアントにメッセージを返す {'Content-Type':'application/json; charset=utf-8'}); setTimeout(notify, 60000); res.end(JSON.stringify(messages)); } }).listen(8192, '127.0.0.1'); console.log('http://127.0.0.1:8192/');     45
  • 46. クライアント側 JavaScript // 初期化処理 // ログインボタン function init() { function set_login_button() { $("#write_form").hide(); $("#login_button").click(function(){ $("#logout_form").hide(); read(); $("#user_name").val(""); $("#write_form").show(); } $("#logout_form").show(); }); // メッセージ出力 } function show(data) { var result = ""; // ログアウトボタン $.each(data, function() { function set_logout_button() { if (this.write_message != null) { $("#logout_button").click(function(){ result += this.user_name + ":" + this.write_message + "<br />"; init(); } }); }); } $("#result").html(result); } $(document).ready(function() { init(); // メッセージの読み込み set_login_button(); function read() { set_logout_button(); // Comet 通知用のコネクション set_write_button(); $.getJSON("read_message.json", null, function(data, status){ }); show(data); // 再度コネクションをはる read(); }); } // メッセージ書き込み function set_write_button() { $("#write_button").click(function(){ var data = {}; data.user_name = $("#user_name").val(); data.write_message = $("#write_message").val(); $.getJSON("write_message.json", data, function(data, status){ show(data); }); }); }     46
  • 47. チープチャット ­WebSocket 版 ­ var http = require('http'), サーバ側 JavaScript fs = require('fs'), ws = require('websocket-server'); // WebSocket サーバの作成 var server = ws.createServer(); // 新規接続 server.addListener("connection", function(connection) { console.log("connect"); // メッセージ受信 connection.addListener("message", function(message){ console.log(message); // 全クライアントにメッセージを送る server.broadcast(message); }); }); // クローズ server.addListener("close", function(connection) { console.log("close"); }); // WebSocket サーバ待ち受け server.listen(8000); // http サーバの作成 http.createServer(function(req, res) { // index.html を表示 fs.readFile(__dirname + '/index.html', function(err, content) { res.writeHead(200, {'Content-Type':'text/html; charset=utf-8'}); res.end(content); }); }).listen(8192, '127.0.0.1'); console.log('http://127.0.0.1:8192/');     47
  • 48. クライアント側 JavaScript var ws = new WebSocket("ws://localhost:8000"); // ログインボタン function set_login_button() { // メッセージ受信時 $("#login_button").click(function(){ ws.onmessage = function(message) { $("#write_form").show(); var data = $.parseJSON(message.data); $("#logout_form").show(); show(data); }); } } // 初期化処理 // ログアウトボタン function init() { function set_logout_button() { $("#write_form").hide(); $("#logout_button").click(function(){ $("#logout_form").hide(); init(); $("#user_name").val(""); }); } } // メッセージ出力 $(document).ready(function() { function show(data) { init(); var result = data.user_name + ":" + set_login_button(); data.write_message + "<br />"; set_logout_button(); $("#result").append(result); set_write_button(); } }); // メッセージ書き込み function set_write_button() { $("#write_button").click(function(){ var data = {}; data.user_name = $("#user_name").val(); data.write_message = $("#write_message").val(); ws.send($.toJSON(data)); }); }     48
  • 49. 参考資料  私のブログ「 shutdown ­h now 」 http://d.hatena.ne.jp/forest1040/  developerWorks 「 Boost application performance using asynchronous I/O 」  http://www.ibm.com/developerworks/linux/library/l­async/  「同期 I/O 」と「非同期 I/O 」の定義、とか  http://d.hatena.ne.jp/hirose31/20070815  非同期 I/O  概説  http://www.slideshare.net/hirose31/aio  epoll  を使った echo  サーバ  http://d.hatena.ne.jp/odz/20070507/1178558340  libev で echo サーバを作る  http://d.hatena.ne.jp/winebarrel/20080309/p2  C10K 問題  http://www.hyuki.com/yukiwiki/wiki.cgi?TheC10kProblem  node.js  のソースぐらい読んでおきたい!  http://d.hatena.ne.jp/edvakf/20101207/1291556433  C++  で node.js  ライブラリを作る・その 2   http://nodejs.g.hatena.ne.jp/edvakf/20101214/1292287495  IT Pro Comet  プッシュ型の Web アプリケーションを作る  http://itpro.nikkeibp.co.jp/article/COLUMN/20080220/294242/  node.js と WebSocket の利用シーン  http://bizria.jp/technical/nodejs­webssocket.html  @IT Node.js でサーバサイド JavaScript 開発入門」  http://www.atmarkit.co.jp/fwcr/index/index_nodejs.html     49
  • 50. ご清聴ありがとうございました。 Node.js は、 JavaScript を使って、比較的手軽 に非同期 I/O のシステムを実装できます。 ◇ 本日のふりかえり  Node.js とは  非同期 I/O とイベントループ  Node.js のアーキテクチャ  インストール  デバッグ環境  Node.js を使ったリアルタイム通信     50