Asial Blog

Recruit! Asialで一緒に働きませんか?

Chrome Appsで簡易Webサーバ構築

カテゴリ :
バックエンド(プログラミング)
タグ :
Tech
HTML5
JavaScript
Chrome

はじめに



今回は、Chrome Appで簡易サーバを作ってみます。Chrome AppsはHTML5を使用して作成され、Chromeをプラットフォームとして動作します。Chromeの画面内部で動くのではなく、アプリケーションごとに個別の画面を持ち、デスクトップ型アプリケーションと同様に振る舞います。様々なAPIが提供され、その種類もどんどん増えてきており、ネイティブアプリに近いことを実装できるようになってきました。一度Chrome Appsを作ってみると、HTML5の世界が広がります。

Chrome AppsのAPIでは、Chromeバージョン24からchrome.socket APIが提供され、UDPやTCPでの通信を実行できるようになりました。さらに、バージョン33からは、chrome.socketがdeprecatedとなり、代わりに以下のAPIが提供されています。


この新しいAPIを使用して簡易Webサーバを構築してみたいと思います。

※ ここではChrome Appsの作り方は知っていることを前提にしています。

各種ファイル



今回のアプリケーションはブラウザでhttp://localhost:3000にアクセスすると、Hello worldと表示する簡易サーバです。このアプリケーションは次の4つのファイルから構成されます。

  • manifest.json: Chrome Appsの基本情報やパーミッションを記述
  • background.js: 画面の構築等を記述
  • index.html: 画面の内容
  • main.js: 画面内部の処理を記述(後述)

manifest.json



まず、Manifestファイルで最低限のパーミッションのみを記述しておきます。今回は、sockets.tcpとsockets.tcpServerを使用します。なお、これらのパーミッションは、"permissions"の外に記述します。

  1. {
  2.     "name": "Asial Blog 2014-04-15",
  3.     "description": "Asial blog sample",
  4.     "version": "1.0",
  5.     "app": {
  6.         "background": {
  7.             "scripts": ["background.js"]
  8.         }
  9.     },
  10.     "sockets": {
  11.         "tcp": {
  12.             "connect": "*"
  13.         },
  14.         "tcpServer": {
  15.             "listen": "*"
  16.         }
  17.     },
  18.     "permissions": []
  19. }

background.js



background.jsでは、単に画面を開きます。200x300の画面をディスプレイ左上に表示します。

  1. chrome.app.runtime.onLaunched.addListener(function(launchData) {
  2.     chrome.app.window.create('index.html', {
  3.         'bounds': {
  4.             'width' : 200,
  5.             'height': 300,
  6.             'top' : 0,
  7.             'left': 0
  8.         }
  9.     });
  10. });

index.html



開かれた画面では、main.jsを読み込むだけとします。アプリを作成する際には、画面の内容や処理結果を表示しますが、今回は全てconsole.logで確認しましょう。

  1. <!DOCTYPE html>
  2. <html>
  3.     <head>
  4.         <script src="main.js"></script>
  5.     </head>
  6.     <body>
  7.     </body>
  8. </html>


サーバの構築



サーバでのデータ送受信の手順は次のようになります。

  1. サーバ用ソケットを作成する(create)
  2. サーバ用ソケットで特定のポートをlistenし、待機状態にする(listen)
  3. サーバ用ソケットへのリクエストが来たら、リクエスト用ソケットを作成する(accept)
  4. リクエスト用ソケットからリクエストの内容を取得し解釈する(receive)
  5. リクエスト用ソケットを使ってレスポンスを返す(send)
  6. リクエスト用ソケットを破棄する(disconnect, close)
  7. 3〜6を繰り返す
  8. サーバを停止し、サーバ用ソケットを破棄する(disconnect, close)

実際にHTTPサーバをChrome Appsで記述すると以下のようになります。

main.js
  1. var serverSocketId;
  2.  
  3. /**
  4.  * サーバ起動
  5.  */
  6. chrome.sockets.tcpServer.create({}, function(createInfo) {
  7.     // サーバ用のソケット
  8.     serverSocketId = createInfo.socketId;
  9.  
  10.     // 3000番ポートをlisten
  11.     chrome.sockets.tcpServer.listen(serverSocketId, '0.0.0.0', 3000, function(resultCode) {
  12.         if (resultCode < 0) {
  13.             console.log("Error listening:" + chrome.runtime.lastError.message);
  14.         }
  15.     });
  16. });
  17.  
  18. /**
  19.  * リクエスト用ソケット作成
  20.  */
  21. chrome.sockets.tcpServer.onAccept.addListener(function(info) {
  22.     if (info.socketId === serverSocketId) {
  23.         chrome.sockets.tcp.setPaused(info.clientSocketId, false);
  24.     }
  25. });
  26.  
  27. /**
  28.  * リクエスト受信
  29.  */
  30. chrome.sockets.tcp.onReceive.addListener(function(info) {
  31.     console.log("Receive: ", info);
  32.  
  33.     // リクエスト確認: ArrayBufferを文字列に変換
  34.     // 本来はヘッダの先頭と、Content-Length等からリクエストの範囲を検出し、
  35.     // 受信データからHTTPリクエストを取り出す必要がある
  36.     var requestText = ab2str(info.data);
  37.     console.log(requestText);
  38.  
  39.     // レスポンス送信
  40.     var socketId = info.socketId;
  41.     var message = 'Hello world';
  42.     var responseText = [
  43.         ' HTTP/1.1 200 OK',
  44.         'Content-Type: text/plain',
  45.         'Content-Length: ' + message.length,
  46.         '',
  47.         message
  48.     ].join("\n");
  49.     chrome.sockets.tcp.send(socketId, str2ab(responseText), function(info) {
  50.         if (info.resultCode < 0) {
  51.             console.log("Error sending:" + chrome.runtime.lastError.message);
  52.         }
  53.  
  54.         // ソケット破棄
  55.         chrome.sockets.tcp.disconnect(socketId);
  56.         chrome.sockets.tcp.close(socketId);
  57.     });
  58. });
  59.  
  60. /**
  61.  * データ受信エラー
  62.  */
  63. chrome.sockets.tcp.onReceiveError.addListener(function(info) {
  64.     console.log("Error: ", info);
  65. });
  66.  
  67. /**
  68.  * 文字列をArrayBufferに変換する(ASCIIコード専用)
  69.  *
  70.  * @param text
  71.  * @returns {ArrayBuffer}
  72.  */
  73. function str2ab(text) {
  74.     var typedArray = new Uint8Array(text.length);
  75.  
  76.     for (var i = 0; i < typedArray.length; i++) {
  77.         typedArray[i] = text.charCodeAt(i);
  78.     }
  79.  
  80.     return typedArray.buffer;
  81. }
  82.  
  83. /**
  84.  * ArrayBufferを文字列に変換する(ASCIIコード専用)
  85.  *
  86.  * @param arrayBuffer
  87.  * @returns {string}
  88.  */
  89. function ab2str(arrayBuffer) {
  90.     var typedArray = new Uint8Array(arrayBuffer);
  91.     var text = '';
  92.  
  93.     for (var i = 0; i < typedArray.length; i++) {
  94.         text += String.fromCharCode(typedArray[i]);
  95.     }
  96.  
  97.     return text;
  98. }

chrome.sockets.tcpServer.createでサーバ用のソケットを生成します。chrome.sockets.tcpServer.listenで3000番ポートでリクエストを待ちます。

そして、chrome.sockets.tcpServer.onAccept.addListenerを使用して、HTTPリクエストを受け取る準備をします。このコールバックは、3000番ポートへのアクセスが発生するたびに呼び出されます。コールバック内で、クライアント用のソケットIDを受け取り、chrome.sockets.tcp.setPausedでソケットの停止状態を解除します。

ここまで来ると、データを受信できます。データの受信は、chrome.sockets.tcp.onReceive.addListenerへ渡したコールバック内部にて行います。受信データはArrayBuffer型から文字列に変換し、その内容を確認しましょう。ヘッダが長い場合や、POSTやKeep-Alive等を考慮する場合、データの取得はもっと複雑になります。今回は短いGETリクエストのみを対象とし、簡略化しています。Webサーバでは、HTTPリクエスト内容を解釈し、レスポンスを決定します。ここでは、どのようなリクエストでも、"Hello world"を返すこととします。データの送信には、chrome.sockets.tcp.sendを使用します。送信データはArrayBuffer型で渡す必要があります。

データの送信が終わったら、リクエスト用ソケットをchrome.sockets.tcp.disconnectで接続を解除し、chrome.sockets.tcp.closeでリクエスト用ソケットを破棄します。HTTPリクエストが届くたびに、データの受信・送信処理とソケット破棄が実行されます。

また、今回は記述していませんが、サーバを停止する場合には、chrome.sockets.tcpServer.diconnectを使用します。さらに、サーバ用ソケットを破棄するには、chrome.sockets.tcpServer.closeを使用します。

Chrome Appsを起動して、ブラウザからhttp://localhost:3000へアクセスして下さい。ブラウザ上にHello worldと表示されていれば成功です。もし上手く動かなければ、アプリを再起動(もしくは再読み込み)してください。アプリ画面上で右クリックし、要素の検証からDeveloper Toolsを表示し、ログを確認すれば、HTTPリクエストが表示されています。

おわりに



Hello worldを返す単純なサーバを構築しました。たったこれだけの内容でも、HTML5の可能性を味わえたと思います。本格的にサーバを構築する場合、HTTPリクエストの解析と解釈、HTTPレスポンスでのデータ送信等を実装していく必要があります。Webサーバを実装できれば、その先のWebSocketサーバなども実装できます。興味があれば、挑戦してみて下さい。