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です。

参考サイト

余談

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

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

0 件のコメント:

コメントを投稿