この記事は tech.kayac.com Advent Calendar 2015 の21日目、 @Gemmbu がお送りします。
NOTE
これ作る前までは WiiU って本体に対してコントローラが複数繋げられると思っていたんですが、一つだけなんですね
Apple TV がリリースされたことですし、何かアプリを作りたいなと思って
- Apple TV は居間で使うから、一人でやるよりみんなでできるのがいい
- みんなで使うのに Apple TV アプリに加えて iOS/Android アプリを落とすのはめんどい
- それなら Apple TV アプリ内に HTTP/WebSocket サーバ立ち上げてそこみんなのスマフォをつなげればいいのでは?
って上記を試せるコードをサクッと書いてみましたので、サンプルプロジェクトを実装しましょう
ソースコードの入手
cocoapods 対応とかしていないので https://github.com/KAMEDAkyosuke/ramaladni からサクッと入手しましょう。
$ git clone git@github.com:KAMEDAkyosuke/ramaladni.git
// サブモジュールも入れましょう
$ cd ramaladni
$ git submodule init
$ git submodule update
NOTE
ramaladni は Diable3 のユニークアイテムです。Socket を開けることができます。http://us.battle.net/d3/en/item/ramaladnis-gift
プロジェクトへの導入
RamaladniSample
という名前で適当な Apple TV アプリを作成して以下の画像のようにプロジェクトへ Ramaladni.xcodeproj をドラッグします。
また、プロジェクトの依存関係を設定します。
HTTPサーバを動かす
AppDelegate.m に以下のようなコードを書きましょう
#import "AppDelegate.h"
#import <Ramaladni/Ramaladni.h> // Ramaladni.framework を使うために必須ですね。
@interface AppDelegate ()
// RAMWebServer のインスタンスをどこかに保持しましょう
@property (nullable, nonatomic, strong) RAMWebServer *webServer;
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
__weak typeof(self) wself = self;
// サーバは処理をブロックしてしまうので、メインスレッドとは別のスレッドで動かしましょう
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// ポートと runLoop を指定します。
// runLoop を指定しない場合メインの runLoop で動作します。
wself.webServer = [RAMWebServer serverWithPort:8080
runLoop:[NSRunLoop currentRunLoop]];
// path に応じてレスポンスを返します
// path は正規表現で表現します。`@".*"` は全てのレスポンスに対応するということになります
RAMHTTPHandler *handler;
handler = [RAMHTTPHandler handleWithPattern:@".*"
blocks:^(RAMHTTPRequest * _Nonnull req, void (^ _Nonnull complete)(RAMHTTPResponse * _Nonnull res))
{
// レスポンスオブジェクトを作って、返します
RAMHTTPResponse *res = [[RAMHTTPResponse alloc] init];
res.header[@"Content-Type"] = @"text/html; charset=UTF-8";
res.statusCode = 200;
res.body = [@"HELLO Ramaladni Server\n" dataUsingEncoding:NSUTF8StringEncoding];
complete(res);
}];
// 作成したハンドラを登録してサーバを起動させます。
wself.webServer.httpHandler = handler;
[wself.webServer start];
});
return YES;
}
簡単に curl でアクセスすると以下のようなレスポンスが得られるはずです
$ curl -v http://localhost:8080/
* Trying ::1...
* connect to ::1 port 8080 failed: Connection refused
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=UTF-8
* no chunk, no close, no size. Assume close to signal end
<
HELLO Ramaladni Server
* Closing connection 0
NOTE
RAMHTTPHandler は next を指定することパターンに合致しない場合、次のハンドラに処理を任せることができます。いわゆる Chain of Responsibility パターンです。NOTE
RAMHTTPFileHandler ってあらかじめ用意した静的なファイルを返すのに便利なクラスもあります
WebSocketサーバを動かす
次は WebSocket サーバです。 AppDelegate.m に以下のようなコードを書きましょう
#import "AppDelegate.h"
#import <Ramaladni/Ramaladni.h>
#import <CommonCrypto/CommonDigest.h> // Sec-WebSocket-Accept を生成するために必要です
@interface AppDelegate ()
@property (nullable, nonatomic, strong) RAMWebServer *webServer;
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
__weak typeof(self) wself = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
wself.webServer = [RAMWebServer serverWithPort:8080
runLoop:[NSRunLoop currentRunLoop]];
// "^/connect" に接続した際には WebSocket へプロトコルをアップグレードさせる処理を書きます
wself.webServer.httpHandler
= [RAMHTTPHandler handleWithPattern:@"^/connect"
blocks:^(RAMHTTPRequest * _Nonnull req, void (^ _Nonnull complete)(RAMHTTPResponse * _Nonnull res))
{
// see: http://tools.ietf.org/html/rfc6455
// お約束の処理です。
// クライアントからsec-websocket-key で送られてきた値と
// "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" を連結し
// sha1 かけて base64 したものをレスポンスヘッダの Sec-WebSocket-Accept に設定します
NSString *key = [NSString stringWithFormat:@"%@%@",
req.header[@"sec-websocket-key"],
@"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"];
NSData *src = [key dataUsingEncoding:NSUTF8StringEncoding];
unsigned char digest[CC_SHA1_DIGEST_LENGTH];
CC_SHA1(src.bytes, (CC_LONG)src.length, digest);
NSData *data = [NSData dataWithBytes:digest length:CC_SHA1_DIGEST_LENGTH];
NSString *base64 = [data base64EncodedStringWithOptions:0];
// レスポンスオブジェクトを作ります
RAMHTTPResponse *res = [[RAMHTTPResponse alloc] init];
res.statusCode = 101;
res.header[@"Upgrade"] = @"websocket";
res.header[@"Connection"] = @"Upgrade";
res.header[@"Sec-WebSocket-Accept"] = base64;
complete(res);
}];
// WebSocket へクライアントから書き込みが行われた際に呼び出される処理を登録します
wself.webServer.webSocketHandler
= [RAMWebSocketHandler handleWithBlocks:^(RAMWebServer * _Nonnull server, RAMWebSocketConnection * _Nonnull conn, RAMWebSocketFrame * _Nonnull frame) {
// 接続しているクライアント全てを取得し、
// 書き込まれたデータをそのまま返す
NSArray<RAMWebSocketConnection*> *connections = [server webSocketConnections];
frame.enableMask = NO;
NSData *data = [frame data];
for(RAMWebSocketConnection *c in connections){
[c writeData:data];
}
}];
[wself.webServer start];
});
return YES;
}
簡単に wscat でアクセスすると以下のようなレスポンスが得られるはずです
$ wscat -c ws://localhost:8080/connect
connected (press CTRL+C to quit)
> foo
< foo
> bar
< bar
>
NOTE
複数の wscat から繋ぐと全てのクライアントへメッセージが送られていることがわかりますNOTE
webSocketHandler の中でframe.enableMask = NO
としている理由は http://tools.ietf.org/html/rfc6455 の 5.2. Base Framing Protocol に書かれています。簡単にいうとクライアントからサーバへ送るデータは同じデータでも毎回ランダムで生成される XOR する必要がありますが、サーバからクライアントへ送るデータは XOR する必要がありません。
これでHTTP/WebSocketサーバを動かすことができました。簡単ですね。 コード行数もこれぐらいしかないので、年末年始にサクッと読んでみるとどうでしょうか?
$ wc -l ./*
26 ./Info.plist
22 ./RAMConnectionProtocol.h
39 ./RAMHTTPConnection.h
178 ./RAMHTTPConnection.m
22 ./RAMHTTPFileHandler.h
74 ./RAMHTTPFileHandler.m
22 ./RAMHTTPHandler.h
58 ./RAMHTTPHandler.m
21 ./RAMHTTPHandlerProtocol.h
26 ./RAMHTTPRequest.h
168 ./RAMHTTPRequest.m
19 ./RAMHTTPResponse.h
69 ./RAMHTTPResponse.m
40 ./RAMWebServer.h
274 ./RAMWebServer.m
35 ./RAMWebSocketConnection.h
178 ./RAMWebSocketConnection.m
34 ./RAMWebSocketFrame.h
103 ./RAMWebSocketFrame.m
21 ./RAMWebSocketFrameStreamParser.h
149 ./RAMWebSocketFrameStreamParser.m
20 ./RAMWebSocketHandler.h
31 ./RAMWebSocketHandler.m
19 ./RAMWebSocketHandlerProtocol.h
30 ./Ramaladni.h
1678 total
まとめ
以外と動作確認をするだけなら HTTP/WebSocket サーバってサクッとかけるんだなぁと。 ボードゲームとかカードゲーム好きなので、これ使って作ってみたい人を探していますので気になる人は声かけてください。
明日は
アドベントカレンダー22日目を担当してくださるのは、川崎さんです。
カヤックではエンジニアを大募集しています
カヤックでは普通の iOS エンジニアだけでなく、やりたいことを実現するためならObjective-CでHTTP/WebSocket サーバをサクッと書いちゃうようなエンジニアを募集しております。 http://www.kayac.com/recruit/career