2012/05/02

ソケット通信で画像を送るときのエンコードまわりについて : Objective-C

Objective-CとRubyのソケットサーバーを使ってチャットアプリを作る際にエンコード周りでハマったのでメモ。

まずは大前提として

1.ソケット通信でテキストメッセージと画像をやりとりする

2.メッセージ送信の主体を表すキー、実行コマンドのキーを付与して、id:cmd:msg というString型の文字列データで通信する
という感じで実装しています。実際にはid/cmd/msgのそれぞれにString型のデータが入っていて、それをコロン「:」でつなげた文字列を使ってサーバーとやりとりする、ということです。

今回はサーバー側、クライアント側で受け取ったデータの内容によって処理を分岐させるために、データをsplitして簡単にキーを抽出できると嬉しいな、ということでString型を採用します。で、問題になったのが、画像の送信。今回は画像データをどうやってソケット通信で送るのか、というところについてです。

例えば

ユーザーAが画像を送信する場合は 「userA:sendimg:画像のString型データ」という文字列をソケットサーバーに送ります。

サーバー側では文字列を解析してUserAが所属しているグループのメンバーに対して受信したものと同じ文字列をブロードキャスト(配信)します。

また、コマンドがsendimgの場合はサーバーに画像を保存する、など含まれるコマンドによってサーバー側でいくつか実行される処理が変わる、という感じ。

テキストデータはそのまま文字列にキーとコロンを付与して送信すればOKだけど、画像については一旦バイナリに変換してからString型に変換する必要がある。なので、画像をNSString型まで持って行って、他のキーと統合した文字列を生成して通信することにする。

画像データにおいては下記のような感じで型変換をしています。
UIImage→NSData(バイナリ)→String→ ソケット通信 →String→NSData→UIImage
NSData→String/String→NSData の部分がMIMEエンコードです。
基本はEメールのエンコードに利用されているものですね。このあたりはもうちょっと勉強しないと、と思っています。


ここからは1つずつ追っていきます。

まずUIImageデータをNSData型に変換

これは簡単。メソッド一発。

NSData* nsData = [[NSData alloc] initWithData:UIImagePNGRepresentation( image )];

逆は下記。あとで受信したデータを戻すときはこっちを使います。
UIImage* image = [[UIImage alloc] initWithData:nsData];

NSDataをString型に変換

さて、メソッドを探してみよう、、ん?Objective-Cにはそんな便利なものは用意されていない!とのこと。。。なので、クラスを自分で定義する必要があります。
下記のサイトで紹介されているソースコードをそのままクラスとして定義すれば動きます。感謝!

[Objective-C] base64エンコード、デコードを実装する & 64進数

というわけでBase64クラスを定義します。


MIMEエンコード/デコード用のクラスを定義

ヘッダーファイル:Base64.h
ここのソースコードをそのままコピー

実装ファイル:Base64.m
ここのソースコードをそのままコピー
※ファイル冒頭のインポートするファイル名は適宜ヘッダーファイル名に変えてください。


使い方も丁寧に載せていただいてますのでリンク貼っておきます。
[Objective-C] base64エンコード、デコードを実装する & 64進数(使い方)

ちなみにObjective-Cはネットの情報が間違っていたり、Xcodeのバージョンによって動かなかったり、ARCを利用しているかどうかで扱いが変わったり、などなど、、色々苦労します。。

でもこのソースコードはそのまま動くので、そのまま書けばOK。
クラスを定義したら実際にメソッドを使ってみましょう。

エンコードしてみる

NSString *msg = [nsData base64String];

これだけでOK。無事にNSString型に変換されました。
そして逆向きの場合は下記です。
NSData *nsData = [NSData dataWithBase64String:msg];

というわけで画像データのUIImage→NSData→NSStringまでの変換が完了しました。

エンコードした文字列を使って通信に使う文字列を生成

あとはキーとなる文字列と統合してソケットに送るメッセージ文字列を生成します。

NSString* key = @"userA";
NSString* cmd = @"sendimg";
NSString* str = [NSString* stringWithFormat:@"%@:%@:%@", uid, cmd, msg];
生成したstr文字列をサーバーに送ると色々処理してくれる、という感じにします。具体的にはコロンでSplit(分割)してcmdを参照して場合分けして、該当処理のメソッドを実行して、、という感じですね。

ソケット通信クライアント側の実装

参考までに、ソケット通信のクライアント側は次のような感じで書いてます。(ちなみにサーバー側はRubyです。)
- (BOOL)socketConnector:(NSString*)uid cmd:(NSString*)type data:(NSData*)msg  {
    NSString *normalBase64String = [msg base64String]; //image | NSData -> NSString
    NSString* msgToSocket = [NSString stringWithFormat:@"%@:%@:%@\n", uid, type, normalBase64String];
    NSUInteger bufferCount = sizeof(int) * ([msgToSocket length] + 1);
    const char *cMsgToSocket = malloc(bufferCount);

    if ([msgToSocket getCString:cMsgToSocket
                      maxLength:bufferCount
                       encoding:NSUTF8StringEncoding]) {
    }
    
    CFSocketContext CTX;
    CTX.version = 0;
    CTX.info = (__bridge void*)self;
    CTX.retain = NULL;
    CTX.release = NULL;
    CTX.copyDescription = NULL;
    
    CFSocketRef client;
    client = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketDataCallBack, (CFSocketCallBack)DataCallBack, &CTX);
    
    CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource(kCFAllocatorDefault, client, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), sourceRef, kCFRunLoopCommonModes);
    CFRelease(sourceRef);
    
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_len = sizeof(addr);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(ポート);
    addr.sin_addr.s_addr = inet_addr("IPアドレス");
    
    NSData *address = [NSData dataWithBytes:&addr length:sizeof(addr)];
    CFSocketError error = CFSocketConnectToAddress(client, (__bridge CFDataRef)address, 2);
    
    if (!error) {
        NSData *sendData = [NSData dataWithBytes:cMsgToSocket length:strlen(cMsgToSocket)];
        CFSocketSendData(client, (__bridge CFDataRef)address, (__bridge CFDataRef)sendData, 10);
        free(cMsgToSocket);
        return YES;
    } else {
        [self showAlert:@"Error002" text:@"Socket Connection2 error"];
        return NO;
    }
}

コールバック関数の実装

また、上記メソッド内でコールバック関数のDataCallBackを起動してます。この部分はC言語で記述します。

static void DataCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void* data, void* info) {
    NSString* socketRcvMessage = [NSString stringWithCString:CFDataGetBytePtr((CFDataRef)data) encoding:NSUTF8StringEncoding];
    [(__bridge chatwindowViewController*)info rcvJudge:socketRcvMessage];
    return;
}
データを受信するとコールバック関数 DataCallBack が呼ばれて受信した文字列をObjective-Cのメソッドに渡します。ちなみにこのようにC言語とObjective-Cとの間で値の受け渡しをする際にはbridgeをしてあげて渡す必要があります。このあたりもハマったのですが、長くなりそうなので、今回はデータの変換に話を留めます。

あとは受信した文字列を解析して、画像だったら画像の表示メソッドを呼ぶ、テキストだったら、、といった感じで処理をすればOKです。

参考サイト

余談

日々いろんなサイトを参考にさせていただいていますが、難しいサイトが多いんですよね、、内容が難しいのもそうだけど、端折られてる部分が初心者にはわからなくて、理解ができない、ということが多かったです。。分かってる人が見たら脳内補完が働くのでそれでもわかるのだと思いますが、、。今みたら「あぁそういうことだったのか、、そう言ってくれれば、、」と感じることも多いです。とはいえ、全部書くわけに行かないので、レベルを想定して書くしかないのですが、、

とりあえず僕個人としては初心者の辛い気持ちが痛いほどわかったので、このブログでは「んなこたぁわかってるよ!」と突っ込まれるくらい丁寧にブログを書いていこう、なんてことを思いましたとさ。。

2012/04/02

インターフェースビルダーを使わずに画面を生成:Objective-C

画面が表示されるまでの内部的な動きを理解するために、インターフェースビルダーを使わずにベタ書きで画面を生成してみる。

画面を描画するためには大きく分けて次の処理が必要になる。
1.ウィンドウの生成と初期化 2.ビューの生成と初期化 3.ウィンドウにビューを追加して表示
全体像としては、ウィンドウは基本1つだけ存在し、その上に複数のビューとコンポーネントが乗る、というイメージ。 ウィンドウの生成やビューの呼び出しはAppDelegateファイル内に記述する。実際のビューの中身についてはビューを描画するクラスを作って別途定義する。 AppDelegateファイルの全体像は下記。
#import "StudyAppDelegate.h"
#import "HelloWorld.h"

@implementation StudyAppDelegate

@synthesize window = _window;

- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    _window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];  //ウィンドウの生成、初期化
    _window.backgroundColor = [UIColor whiteColor];  //背景色の指定
    [_window makeKeyAndVisible];  //ウィンドウの表示

    CGRect bounds = [[UIScreen mainScreen] bounds];  //スクリーンサイズの取得
    bounds.origin.y += 20;  //ビューの配置場所を指定
    bounds.size.height -= 20;  //ビューのサイズを指定
    UIView* view = [[[HelloWorld alloc] initWithFrame:bounds] autorelease];  //ビューを生成
    [_window addSubview:view];  //ビューの表示
    
    return YES;
}

- (void)dealloc {
    [_window release];
    [super dealloc];
}

@end
1行ずつの処理の内容を羅列すると、
・起動直後に呼ばれるメソッドで下記を処理 - ウィンドウの生成、初期化 - 背景色の指定 - ウィンドウの表示 - スクリーンサイズの取得 - ビューの配置場所を指定 - ビューのサイズを指定 - ビューを生成 - ビューの表示 - メソッドの返り値としてYESを返す。 ・メモリ開放のメソッドを書く。
1行ずつ見ていく。 まずは土台となるウィンドウを作るところから。

ウィンドウの生成と初期化

_window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
まずはUIWindowクラスのallocメソッドでメモリを確保して、initWithFrameメソッドで初期化。初期化時に画面サイズを指定したいので、UIScreenクラスのmainScreenメソッドでメインスクリーンのオブジェクトを取得し、boundsプロパティで画面サイズを取得する。今回はウィンドウサイズ=スクリーンサイズとする。ここまででウィンドウの生成と初期化が完了。

ウィンドウ背景色を指定する

_window.backgroundColor = [UIColor whiteColor];
インスタンス変数_windowのbackgroundColorプロパティに[UIColor whiteColor]を代入すると背景色が指定できる。 この時点でウィンドウの準備が完了したので、

ウィンドウを表示する

[_window makeKeyAndVisible];
つぎはウィンドウの上に乗せるビューの生成と初期化。 ビューの配置する場所や大きさを事前に指定するところから。

改めてウィンドウのサイズを取得しておく

CGRect bounds = [[UIScreen mainScreen] bounds];
CGRect型の変数boundsを用意しつつ、再度[[UIScreen mainScreen] bounds]でメインスクリーンのサイズを改めて取得。(ウィンドウ生成する前の時点で変数を用意して格納しておけば2回記述する必要ないな、、) ※サイズ指定(CGRect CGPoint CGSize) - iPhoneアプリ開発の虎の巻 : http://goo.gl/rJR6H

ビューの配置場所を指定する

bounds.oringin.y += 20;
左上の頂点を始点としてピクセル単位で指定する。メニューバーの下から表示するように値に20を足す。

ビューの縦の大きさを指定する

bounds.size.height -= 20;
ビューを配置する位置を20ピクセル下に移動したので、ビューの全体の縦の大きさは20ピクセル小さくする。(そうしないと画面からはみ出してしまうので。)

ビューの生成と初期化

UIView* view = [[[HelloWorld alloc] initWithFrame:bounds] autorelease];
HelloWorldクラスのインスタンスを生成して、配置場所と大きさを格納したbounds変数を引数として初期化する。 HelloWorldクラスはビューの詳細について書かれているのかな?という感じがする。実際に見てみると、、
#import "HelloWorld.h"

@implementation HelloWorld

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor purpleColor];
    }
    return self;
}

- (void)drawRect:(CGRect)rect {
    UIFont* font = [UIFont systemFontOfSize:24];
    [@"Hello, World!" drawAtPoint:CGPointMake(0,0) withFont:font];
}
@end
こんな感じ。詳しく見ていく。

initWithFrame:メソッドをオーバーライド

- (id)initWithFrame:(CGRect)frame {
}
HelloWorldクラスはUIViewクラスを継承しているので、initWithFrame:メソッドをオーバーライドすることによって初期化処理ができるようになる。 次に、初期化処理の具体的な内容。

親クラスの同名メソッドを読んでオブジェクトを生成してプロパティを指定

self = [super initWithFrame:frame];
if (self) {
    self.backgroundColor = [UIColor purpleColor];
}
return self;
ビューを作ったら何か表示しましょう、ということで描画メソッドを定義する。 HelloWorldクラスはUIViewクラスを継承しているので、drawRect:メソッドをオーバーライドして描画処理を実行できる。

drawRect:メソッドをオーバーライド

- (void)drawRect:(CGRect)rect {
}
このdrawRect:メソッドはビューの再描画が必要なタイミングで自動的に呼び出される。

文字列を描画してみる

UIFont* font = [UIFont systemFontOfSize:24];
[@"Hello, World!" drawAtPoint:CGPointMake(0,0) withFont:font];
1行目でフォントサイズの指定用にUIFont型のfont変数にフォントサイズを格納し、2行目でNSStringクラスのdrawAtPoint:withFontメソッドで文字列を描画する。 ちなみにsystemFontOfSizeメソッドはUIFontのクラスメソッド。また、drawAtPointの引数はCGpoint型で引き渡す必要があるため、CGpointMake関数を使ってCGPointを生成している。 HElloWorld.mが確認できたのでAppDelegate.mファイルの続きに戻る。

ビューを表示する

[_window addSubview:view];
addSubviewメソッドでウィンドウにビューを追加する。 ここまでが画面描画までの一連のプロセス。

2012/03/27

いろいろ用語集

tmux

サーバー側でターミナルを起動して、そこに対してリモートから接続するイメージ。
もし本番環境で作業中にネットワークが切れても大丈夫。
スクリーンの縦分割がデフォルトでできるというのが使いやすいところ。

※tmux : http://goo.gl/DlKn

クロスサイトリクエストフォージェリ対策

昔mixiで話題になったぼくはまちちゃん、がそう。

1. 攻撃者が、攻撃用の Web ページを作成して WWW 上に公開する。(WWW上ででなくとも、未対策のHTMLメーラーにウェブページをHTMLメールとして送信するだけでもよい。)
2. 第三者が、攻撃用の Web ページにアクセスする。
3. 第三者は、攻撃者が用意した任意の HTTP リクエストを送信させられる。

4. 送信させられた HTTP リクエストによって、攻撃者の意図した操作が行われる。 (ここで「第三者」とは、被攻撃サイトに意図せずアクセスさせられると言う意味で用いている。)

防ぐ方法としては一連の流れを紐付けるようにセッション情報を紐付ける、という方法がある。

Railsではライブラリとして提供されているものがあるので、それを組み込む方法で対応したほうが自前で作成するよりも信頼性が高い。

クロスサイトリクエストフォージェリ - Wikipedia : http://goo.gl/bFhn

カピストラーノ

クライアントからSSHでサーバーにリモートアクセスしてシェルを実行するどいう動作をRubyをつかって自動化できるツール。
元々はRailsのデプロイの緩和のために作られたが、他にも色々と使える。

カピストラーノタスクはRubyで記述する。また、カピストラーノはcapfileから指示を読む。capfileはただのテキストファイル。

capコマンドで実行できる。また、役割を与えることでサーバーごとに処理を変えることもできる。

※カピストラーノ(Capistrano)を使ってみよう!(Basic編) : LR Labs : 株式会社ライブレボリューション アリエスユニット : http://goo.gl/SHjeF

chef-solo

chefもあるが、chefサーバーを立てないといけないので、chef-soloが比較的お手軽な感じ。
Rubyでレシピを書ける、というのがいいらしい。

※chef-soloで作業環境構築の自動化 | ひげろぐ : http://goo.gl/MfE3g

IRC【Internet Relay Chat】

基本的にはチャットシステム。Skypeなどとは違い、ID認証を必要とせずサーバー情報をIRCクライアントに指定するだけで接続することができる。また、基本は多人数でのテキストチャットがメイン機能。
IRCのサーバー側はLinuxベースでの構築が必要。
会社内や学校内のサーバーにIRCを構築してクローズドのチャットシステムを作ることができる。

※IRCとSkypeの違いってなんですか? - Yahoo!知恵袋 : http://goo.gl/IJ94V

継続的インテグレーション (Continuous Integration, CI)

継続的インテグレーションとは、ソフトウェアの品質改善・納期短縮のためのソフトウェア・エンジニアリングの習慣の集合である。その原則は、開発の連続的な全行程が終わってから品質管理を行うという古い慣行をやめ、成果物の諸小部分に対して頻繁に品質管理を行うことである。

※継続的インテグレーションとは - はてなキーワード : http://goo.gl/sY1yw

Jenkins

CIツール。
ソースコードの統合、テストの自動化、メトリクスの自動取得、ビルド完了時の画面を変えたりなどの愉快な機能もw
詳細は下記。

※「Hudson」改め「Jenkins」で始めるCI(継続的インテグレーション)入門 (1/4) - @IT : http://goo.gl/6F4d7

リファレンス

「変数を参照するもの」の意味。参照したい変数のアドレスのことで、C言語ではポインタと呼ばれたりする。
ただしC言語のポインタと同じに考えてはいけない。微妙に別のものである。
このリファレンスを通して、参照している変数を呼び出すことをデリファレンスという事も合わせて覚えておきたい。

※リファレンスとは - はてなキーワード : http://goo.gl/Dr8vb

ガングリア

下記のdstatと同様、サーバーリソースをグラフ化してくれるツール

dstat

サーバーリソースをグラフ化してくれるツール。
他にもいくつかツールが有るようだが、色々と統合して表示してくれる、ということで便利らしい。

※dstatの万能感がハンパない - (ひ)メモ : http://goo.gl/ETRui

iostat -dkxではIOPSやバイト単位の値など、I/Oに関する詳細は確認できますが、CPU使用率など他の統計値は同時に見ることができません。
vmstatはメモリの状況やブロック数単位のI/O状況は見られますが、バイト単位のI/O状況やネットワークの送信、受信バイト数を見ることはできません。
ターミナルを複数上げてvmstatとiostatをそれぞれ実行してもいいのですが、できれば一つのターミナルでいろいろな値を見たい。というわけで、dstatの出番です。
□MTGでの会話メモ IOスタットとVMスタットを合わせた感じ
1秒ごとにモニタリングしてくれる。
最初はガングリアでもいいが、ガングリアは15秒とか30秒ごとに見る感じ。
脳内マージしなくても良くなるのは嬉しい。
方針としてはディスクIOをなくすようにしていくことが目標。

インスタンス変数の定義はプロパティ宣言が自動生成してくれる:Objective-C

いろんなサンプルプログラムを見ているとクラスを定義する際のインスタンス変数の定義部分で色々な定義のされ方があって混乱したので、調べてみた。

そもそもは

インスタンス変数はクラス定義の.hファイル内の@interfaceから始まるコンパイラディレクティブ内の{}の中に記述する、とういうのが基本でした。が、Objective-C2.0からコンパイラがclang(詳細についてはエントリ末尾の参考サイト参照)になって賢くなった、ということらしく、プロパティで変数が宣言されていれば自動生成させるだけでOK、ということになったそうです。

なので、これまではインスタンス変数を宣言しつつ、プロパティでももう一回インスタンス変数についてのプロパティ宣言をしていたのですが、{}内の記述は消しちゃってもいい、といよりもむしろ{}ごと消しちゃいなさい、ということみたいです。具体的には下記みたいな感じです。

これまでは

@interface hogehoge : NSObject
{
    NSString *str1 ;  //インスタンス変数str1を定義
    NSString *str2;   //インスタンス変数str2を定義
}

@property (nonatomic) NSString *str1;  //str1のプロパティ宣言
@property (nonatomic) NSString *str2;  //str2のプロパティ宣言
@end

Objective-C2.0以降は

@interface hogehoge : NSObject
@property (nonatomic) NSString *str1;  //str1のプロパティ宣言
@property (nonatomic) NSString *str2;  //str2のプロパティ宣言
@end
すっきり!

また、上にも書きましたが、基本的にプロパティ宣言で済んでしまった場合は{}も消しましょう、ということみたいです。 iOS Developer Libraryの「Objective-C プログラミング言語」ファイルのP.35にも下記のように書いてあります。
歴史的には、インターフェイスにはクラスのインスタンス変数宣言が必要でした。(中略)インスタンス変数は実装詳細であり、通常、クラス自身の外からアクセスされることはありません。さらに、実装ブロック内に宣言すること、あるいは宣言済みプロパティから自動生成させることも可能です。したがって通常は、インスタンス変数宣言をパブリックインターフェイスで行うべきではないので、波括弧も省略してください。
プロパティ宣言が必要ないインスタンス変数を定義したい場合にはヘッダファイル(.hファイル)に書くのではなくて、実装ファイル(.mファイル)内の@implementation内に{}を追加してその中に記述するのが今風(?)みたいですw


参考サイト

イマドキっ子の Objective-C | cockscomb.info
Objective-C入門その7:アクセサメソッドをマスターする|サルにもできるiPhoneアプリの作り方
Clang - Wikipedia

2012/03/26

ラッパークラスについて:Objective-C

ラッパークラスってなに?

まず前提として、int型等の基本的な変数はオブジェクト化されていません。ただし、Objective-Cで用意されている基本的なクラス群の中には引数を必ずオブジェクト型で渡さないといけないメソッドがたくさん定義されています。

そこで、int型などの数値をオブジェクトとして扱う必要があるため、その時のためにラッパークラスが用意されています。
つまり、ラッパークラスは数値などをオブジェクト化するためのクラス、ということになります。

ラッパークラスNSNumbrerでラップしてみる

まずは
NSNumber *wrappedInt = [NSNumber numberWithInt:123]; //インスタンス生成メソッドを利用してオブジェクト生成
もしくは
NSNumber *wrappedInt = [[NSNumber alloc] initWithInt]];  //イニシャライザを利用してオブジェクト生成
でInt型のラッパークラスのインスタンスを生成する。

※上記の2つにはメモリ管理の視点から見ると違いがあるらしいので、後々勉強が必要。

ラップされた値を取り出す

NSNumberオブジェクトからラップされている値を取り出すときにはいろんな型で取り出すことができる。 たとえば、 [wrappedInt intValue] とか [wrappedInt doubleValue] とか [wrappedInt stringValue] とかで取り出せる。