openFrameworksのofxOSCでデカい(65KB以上くらい)データを送る

openFramworksのofxOSCでは画像を送ることが可能だが、サンプルにも書かれている通り、一定以上のサイズはサンプルのコードでは送る事ができない。具体的には大体65000Byte以上くらいなのだが、これはOSCが利用しているUDPペイロードサイズの制限(より正確にはIPパケット)によるものだ。ちょうどよい解説が@ITにあったので引用しておく。

1つのUDPパケットで運ぶことのできるデータ(「ペイロード(荷物)」という)の長さは、下位層のIPパケットの長さの制約を受ける。(標準の)IPパケットでは、1回の送信で、最大では65515bytes(65535bytesから、IPヘッダの最低サイズ20bytesを引いたもの)までのデータを送信することができる(IPヘッダ・オプションが付くと、さらに小さくなる)。そのため、1つのUDPパケットで送信することのできる最大ペイロード・サイズは、65515bytesからUDPヘッダのサイズ(8bytes)を減算した、65507bytesまでとなる。このため、この「長さ」フィールドの値は、8(データが空の場合)~65515となる。
基礎から学ぶWindowsネットワーク:第13回 データグラム通信を実現するUDPプロトコル (3/4) - @IT

今回、これを超える大きさのデータのやり取りをしたいと思って実装したので詳細を書いておく。oFのバージョンは0.9.8。

方法の概略

送信側でデータをサイズ制限以下に分割してOSCメッセージにして送信、受信側でデータを結合する。
OSCメッセージには、データ自体とデータの開始フラグと終了フラグをつけておくことで、受信側でデータをどのように結合すればいいか分かるようにした。

コード

全体のファイルはGithubに置いた。macOS 10.12.4 のXcode 8.3 で開発した。相手先のIPアドレスはsettings.xmlで設定する。
GitHub - torukawanabe/oscImageTransferExample

送信

ofBuffer型の変数imgAsBufferに画像データが読み込まれている。他のデータを考慮しても確実に上記サイズ制限に収まるであろう60000バイトで分割している。

void ofApp::sendImage(){
    img.load(imgAsBuffer);
    
    static const int dividingSize = 60000;
    int numOfSend = (imgAsBuffer.size() / dividingSize) + 1;
    cout << "NOW POST IMG SIZE:" << imgAsBuffer.size() << " ,NUM OF SEND:" << numOfSend << endl;
    
    if (numOfSend == 1) {
        ofxOscMessage m;
        m.setAddress("/image");
        m.addBoolArg(true);
        m.addBoolArg(true);
        
        vector<char> sendBuff = vector<char>(imgAsBuffer.begin(), imgAsBuffer.end());
        m.addBlobArg(ofBuffer(sendBuff.data(), sendBuff.size()));
        
        sender.sendMessage(m, false);
    }else{
        vector<char>::iterator it = imgAsBuffer.begin();
        for (int i=0; i<numOfSend; i++) {
            ofxOscMessage m;
            
            vector<char> sendBuff;
            sendBuff.clear();
            
            m.setAddress("/image");
            if(i == 0){
                m.addBoolArg(true);
                m.addBoolArg(false);
            }else if(i == (numOfSend - 1)){
                m.addBoolArg(false);
                m.addBoolArg(true);
            }else{
                m.addBoolArg(false);
                m.addBoolArg(false);
            }
            
            for(int j=0; j<dividingSize; j++){
                sendBuff.push_back(*it);
                if(it == imgAsBuffer.end()) break;
                it++;
            }
            cout << "sendBuff" << i << ":" << sendBuff.size() << endl;
            m.addBlobArg(ofBuffer(sendBuff.data(), sendBuff.size()));
            sender.sendMessage(m, false);
        }
        
    }
    
    cout << "ofApp:: sending image with size: " << imgAsBuffer.size() << endl;
}

受信側

vector型の変数receivedBufferにOSCメッセージからパースしたデータをいれていく仕組みで実装した.

void ofApp::updateOSC(){
    while(receiver.hasWaitingMessages()){
        // get the next message
        ofxOscMessage m;
        receiver.getNextMessage(m);
        if(m.getAddress() == "/image" ){
            bool isFirst = m.getArgAsBool(0);
            bool isLast = m.getArgAsBool(1);
            
            if(isFirst){
                receivedBuffer.clear();
            }
            
            ofBuffer buff = m.getArgAsBlob(2);
            for (char c: buff){
                receivedBuffer.push_back(c);
            }
            
            // ofBufferが最後に0を挿入するので末尾を削除
            receivedBuffer.pop_back();
            
            if(isLast){
                ofBuffer imgBuff = ofBuffer(receivedBuffer.data(), receivedBuffer.size());
                receivedImg.load(imgBuff);
            }
        }
    }
}

注意点

ofBufferは内部で持つvectorで最後に0を勝手に挿入する*1ので、データの結合時に削除する必要がある。また、UDPはパケットの送達確認や到着順序は保証されないが、今回の実装ではパケットの不着や順序の入れ替わりの可能性を考慮していないので、注意してほしい。またパフォーマンスも考慮していない。

*1:NULL終端のため