sos の 作業メモ

プログラミングや英会話学習、マイルや旅行、日常生活など。最近はWebFormなASP.NETのお守りがお仕事です。

日々の生活にhappyをプラスする|ハピタス Gポイント

NSStreamでクライアントとしてTCPソケット通信を行う その3 完成

その2の続き

まだ完全にテストはできていませんが、GCD専用で、メソッドが同期モードな、ソケット通信を行うクラスが出来上がりました。

クラス定義

命名規則等は慣れていないのですが、こんな感じで。

@interface TFTCPConnection : NSObject<NSStreamDelegate>

@property (readonly, nonatomic) NSString* hostname;
@property (readonly, nonatomic) UInt32 port;
@property (nonatomic) NSInteger timeoutSec;

- (id)initWithHostname:(NSString*)hostname port:(UInt32)port timeout:(int)timeoutSec;

- (BOOL)openSocket;
- (void)closeSocket;
- (BOOL)readData:(NSMutableData*)data length:(NSUInteger)len;
- (BOOL)writeData:(const void*)data length:(NSUInteger)len;

@end

プロパティ

前の2つがreadonlyなのは、同じところと何度もやりとりを行うことを想定したクラスだから。 プロパティにする必要は無いような気もするのですが、ログ出力とかで使うかもしれないので念のため保持してます。

  • hostname 接続先のホスト名
  • port 接続先のポート
  • timeoutSec open/read/write時のタイムアウト[秒]

メンバ変数

プロパティの自動でシンセサイズ(合成でしたっけ?)されるものにあわせて、他のメンバもアンダースコアで始めることにしてます。

@implementation TFTCPConnection
{
    dispatch_semaphore_t _semaphore;

    NSInputStream* _readStream;
    NSOutputStream* _writeStream;
}

同期モードで動かす為の要のセマフォと、read/writeをそれぞれ受け持つstream

指定イニシャライザ

ホスト名とポート番号、タイムアウトの秒を指定して初期化するようにしてます。

- (id)initWithHostname:(NSString*)hostname port:(UInt32)port timeout:(int)timeoutSec
{
    self = [super init];
    if(self){
        _hostname = hostname;
        _port = port;
        _timeoutSec = timeoutSec;

        _readStream = nil;
        _writeStream = nil;
    }
    return self;
}

デリゲート

NSStreamDelegateのメソッド Input/OutputのStreamの状態が変化した時に呼び出されるDelegateメソッド。セマフォでwaitしているスレッドをたたき起こしているだけです。

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
    assert(aStream == _readStream || aStream == _writeStream);
    NSLog(@"handleEvent: %u",eventCode);
    dispatch_semaphore_signal(_semaphore);
}

タイムアウト計算用の補助メソッド

秒で指定された値を、dispatch_semaphore_waitに指定するためのdispatch_time_tに変換するメソッドです。実はTCP自体のタイムアウト等の設定は弄っておらず、セマフォの獲得待ち時間を利用した擬似的なタイムアウト処理だったりします。

- (dispatch_time_t)innerDispatch_time
{
    if(_timeoutSec > 0){
        return dispatch_time(DISPATCH_TIME_NOW, (_timeoutSec * NSEC_PER_SEC));
    }else if(_timeoutSec == 0){
        return DISPATCH_TIME_NOW;
    }else{
        return DISPATCH_TIME_FOREVER;
    }
}

ソケットオープン

名前解決をして、CFStreamを生成し、それをNSStreamにキャストして、delegateやらRunLoopやらを設定する処理。同期用のセマフォもここで作ります。接続が完了するか、エラーが発生するまでこの関数からは戻りません。 ストリームのRunLoopには mainスレッドを指定しています。

- (BOOL)openSocket
{
    BOOL ret = NO;

    if(_semaphore) return ret;
    _semaphore = dispatch_semaphore_create(0);
    dispatch_retain(_semaphore);

    CFReadStreamRef readStream;
    CFWriteStreamRef writeStream;

    CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, (__bridge CFStringRef)_hostname,  _port, &readStream,&writeStream);

    _readStream = (__bridge_transfer NSInputStream*)readStream;
    _readStream.delegate = self;
    [_readStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

    _writeStream = (__bridge_transfer NSOutputStream*)writeStream;
    _writeStream.delegate = self;
    [_writeStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

    [_readStream open];
    [_writeStream open];

    dispatch_time_t timeout = [self innerDispatch_time];

    // 読み込みストリームオープン検査
    while(TRUE){
        NSStreamStatus stat = _readStream.streamStatus;
        NSLog(@"_readStream.streamStatus %u",stat);
        if(stat == NSStreamStatusOpen){
            NSLog(@"_readStream open");
            break;
        }else if(stat != NSStreamStatusOpening){
            return ret; // エラー
        }
        if(dispatch_semaphore_wait(_semaphore, timeout)) return ret;
    }
    //  書き出しストリームオープン検査
    while(TRUE){
        NSStreamStatus stat = _writeStream.streamStatus;
        NSLog(@"_writeStream.streamStatus %u",stat);
        if(stat == NSStreamStatusOpen){
            NSLog(@"_writeStream open");
            break;
        }else if(stat != NSStreamStatusOpening){
            return ret; // エラー
        }
        if(dispatch_semaphore_wait(_semaphore, timeout)) return ret;
    }
    return YES;
}

ソケットクローズ

切断し、再接続に備えて後始末を行います。

- (void)closeSocket
{
    if(_writeStream){
        _writeStream.delegate = nil;
        [_writeStream close];
        [_writeStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        _writeStream = nil;
    }
    if(_readStream){
        _readStream.delegate = nil;
        [_readStream close];
        [_readStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        _readStream = nil;
    }
    if(_semaphore){
        dispatch_release(_semaphore);
        _semaphore = nil;
    }
}

データ読み込み

NSMutableData に指定のバイト数のデータを読み込みます。もちろんエラーか読み込みが完了するまでは呼び出し元には戻りません。指定バイト読み込めていないときにセマフォを要求してwaitするようにしているところがミソです。

- (BOOL)readData:(NSMutableData*)data length:(NSUInteger)len
{
    BOOL ret = NO;
    NSInteger leftlen = len;
    if(leftlen <= 0) return YES;
    dispatch_time_t timeout = [self innerDispatch_time];
    while(TRUE){
        NSStreamStatus stat = _readStream.streamStatus;
        if(stat == NSStreamStatusOpen || stat == NSStreamStatusReading){
            if([_readStream hasBytesAvailable]){
                // 読み込み可能
                uint8_t buf[1024];
                NSInteger maxlen = (sizeof(buf) / sizeof(uint8_t)); // バッファサイズ
                if(maxlen > leftlen) maxlen = leftlen;
                NSInteger count = [_readStream read:buf maxLength:maxlen];
                if(count > 0){
                    [data appendBytes:buf length:count];
                    leftlen -= count;
                    if(leftlen <= 0){
                        // 指定バイト読み込めたので終了
                        ret = YES;
                        break;
                    }
                }else{
                    if(count == 0){
                        NSLog(@"readData eof");
                    }else{
                        NSLog(@"readData error %@",_readStream.streamError.description);
                    }
                    break;
                }
            }
        }else{
            NSLog(@"readData error %u",stat);
            break; // エラー
        }
        if(dispatch_semaphore_wait(_semaphore, timeout)){
            NSLog(@"readData timeout");
            break;
        }
    }
    return ret;
}

データ書き出し

指定されたバイトを書き出します。読み込み処理と似たような処理になります。

- (BOOL)writeData:(const void*)data length:(NSUInteger)len
{
    BOOL ret = NO;
    NSInteger leftlen = len;
    if(leftlen <= 0) return YES;
    dispatch_time_t timeout = [self innerDispatch_time];
    while(TRUE){
        NSStreamStatus stat = _writeStream.streamStatus;
        if(stat == NSStreamStatusOpen || stat == NSStreamStatusWriting){
            if([_writeStream hasSpaceAvailable]){
                // 書き出し可能
                NSInteger count = [_writeStream write:(data + (len - leftlen)) maxLength:leftlen];
                if(count >= 0){
                    leftlen -= count;
                    if(leftlen <= 0){
                        ret = YES;
                        break;
                    }
                }else{
                    NSLog(@"writeData error %@",_writeStream.streamError.description);
                    break;
                }
            }
        }else{
            NSLog(@"writeData error %u",stat);
            break; // エラー
        }
        if(dispatch_semaphore_wait(_semaphore, timeout)){
            NSLog(@"writeData timeout");
            break;
        }
    }
    return ret;
}

クラス実装終了

呼び出し方

こんな感じ。 StreamのRunLoopをmain threadにしているので、呼び出す方はglobal queueで実行する必要があります。メインスレッドを使っちゃうと、セマフォのwaitでRunLoop自体がとまってえらいことになります。 (まぁ、通信処理をメインスレッドでやるようなことはないでしょうけど)

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    _conn = [[TFTCPConnection alloc] initWithHostname:@"hoge.hoge.hoge" port:12345 timeout:30];
    if([_conn openSocket]){
        NSLog(@"openSocket success");
        unsigned int intval = 0x12345678u;
        unsigned int swapval = NSSwapHostIntToBig(intval);

        if([_conn writeData:&swapval length:sizeof(swapval)]){
            NSLog(@"writeData success");
        }else{
            NSLog(@"writeData failure");
        }
        NSMutableData* data = [NSMutableData data];
        if([_conn readData:data length:4]){
            NSLog(@"readData success");
        }else{
            NSLog(@"readData failure");
        }
    }else{
        NSLog(@"openSocket Error");

    }
    [_conn closeSocket];
});

あー長かった。 これをTLS対応にするにはもう一工夫が必要なのですが、とりあえず今回はここまで。

おまけへ