Chrome Appsで簡易Webサーバ構築

はじめに

今回は、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"の外に記述します。


{
    "name": "Asial Blog 2014-04-15",
    "description": "Asial blog sample",
    "version": "1.0",
    "app": {
        "background": {
            "scripts": ["background.js"]
        }
    },
    "sockets": {
        "tcp": {
            "connect": "*"
        },
        "tcpServer": {
            "listen": "*"
        }
    },
    "permissions": []
}

background.js

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


chrome.app.runtime.onLaunched.addListener(function(launchData) {
    chrome.app.window.create('index.html', {
        'bounds': {
            'width' : 200,
            'height': 300,
            'top' : 0,
            'left': 0
        }
    });
});

index.html

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


<!DOCTYPE html>
<html>
    <head>
        <script src="main.js"></script>
    </head>
    <body>
    </body>
</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


var serverSocketId;
/**
 * サーバ起動
 */
chrome.sockets.tcpServer.create({}, function(createInfo) {
    // サーバ用のソケット
    serverSocketId = createInfo.socketId;
    // 3000番ポートをlisten
    chrome.sockets.tcpServer.listen(serverSocketId, '0.0.0.0', 3000, function(resultCode) {
        if (resultCode < 0) {
            console.log("Error listening:" + chrome.runtime.lastError.message);
        }
    });
});
/**
 * リクエスト用ソケット作成
 */
chrome.sockets.tcpServer.onAccept.addListener(function(info) {
    if (info.socketId === serverSocketId) {
        chrome.sockets.tcp.setPaused(info.clientSocketId, false);
    }
});
/**
 * リクエスト受信
 */
chrome.sockets.tcp.onReceive.addListener(function(info) {
    console.log("Receive: ", info);
    // リクエスト確認: ArrayBufferを文字列に変換
    // 本来はヘッダの先頭と、Content-Length等からリクエストの範囲を検出し、
    // 受信データからHTTPリクエストを取り出す必要がある
    var requestText = ab2str(info.data);
    console.log(requestText);
    // レスポンス送信
    var socketId = info.socketId;
    var message = 'Hello world';
    var responseText = [
        ' HTTP/1.1 200 OK',
        'Content-Type: text/plain',
        'Content-Length: ' + message.length,
        '',
        message
    ].join("\n");
    chrome.sockets.tcp.send(socketId, str2ab(responseText), function(info) {
        if (info.resultCode < 0) {
            console.log("Error sending:" + chrome.runtime.lastError.message);
        }
        // ソケット破棄
        chrome.sockets.tcp.disconnect(socketId);
        chrome.sockets.tcp.close(socketId);
    });
});
/**
 * データ受信エラー
 */
chrome.sockets.tcp.onReceiveError.addListener(function(info) {
    console.log("Error: ", info);
});
/**
 * 文字列をArrayBufferに変換する(ASCIIコード専用)
 *
 * @param text
 * @returns {ArrayBuffer}
 */
function str2ab(text) {
    var typedArray = new Uint8Array(text.length);
    for (var i = 0; i < typedArray.length; i++) {
        typedArray[i] = text.charCodeAt(i);
    }
    return typedArray.buffer;
}
/**
 * ArrayBufferを文字列に変換する(ASCIIコード専用)
 *
 * @param arrayBuffer
 * @returns {string}
 */
function ab2str(arrayBuffer) {
    var typedArray = new Uint8Array(arrayBuffer);
    var text = '';
    for (var i = 0; i < typedArray.length; i++) {
        text += String.fromCharCode(typedArray[i]);
    }
    return text;
}

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サーバなども実装できます。興味があれば、挑戦してみて下さい。