はじめに
今回は、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>
サーバの構築
サーバでのデータ送受信の手順は次のようになります。
- サーバ用ソケットを作成する(create)
- サーバ用ソケットで特定のポートをlistenし、待機状態にする(listen)
- サーバ用ソケットへのリクエストが来たら、リクエスト用ソケットを作成する(accept)
- リクエスト用ソケットからリクエストの内容を取得し解釈する(receive)
- リクエスト用ソケットを使ってレスポンスを返す(send)
- リクエスト用ソケットを破棄する(disconnect, close)
- 3〜6を繰り返す
- サーバを停止し、サーバ用ソケットを破棄する(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サーバなども実装できます。興味があれば、挑戦してみて下さい。