2

(UPDATED) I am trying to read a large file ( a video or a picture) and send it to a remote server via a SOAP request. I need to encode the data as a Base64 string. I am trying to do this as follows:

  • Create a template xml for the SOAP request that will go "around" the base64 encoded data

  • push the first part of the SOAP xml into a buffer

  • open the video file and encode it in chunks and push each encoded chunk into the buffer

  • finally, push the second part of the SOAP xml

To be able to "enqueue" parts as above, I am trying to use GCDAsyncSocket with its buffering capabilities. I figure that since GCDAsyncSocket operates on TCP level, I need to write the HTTP POST header myself. So, there are many moving parts which I only vaguely understand and I might be doing it all incorrectly. But my socket never seems to even take off and I am not even sure how to debug it. Here is my relevant code, try to see if you spot any obvious errors:

NSString *soapBody = ...; //Create the SOAP xml here with the part in the middle reserved for the Base64 encoded data (marked with string "CUT_HERE";
NSArray *soapBodyArray = [soapBody componentsSeparatedByString:@"CUT_HERE"];
self.p_soapBodyPart1 = [soapBodyArray[0] dataUsingEncoding:NSUTF8StringEncoding];
self.p_soapBodyPart2 = [soapBodyArray[1] dataUsingEncoding:NSUTF8StringEncoding];
socketQueue = dispatch_queue_create("socketQueue", NULL);//socketQueue is an ivar
self.p_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:socketQueue];//create the socket
NSError *err = nil;
if (![p_socket connectToHost:myURL onPort:80 error:&err]) // Asynchronous!
{
   NSLog(@"I goofed: %@", err);
   return;
}
NSString* httpHeader = [NSString stringWithFormat:@"POST %@ HTTP/1.1\r\nHost: %@\r\nAccept-Encoding: gzip, deflate\r\nContent-Type: text/xml\r\nAccept-Language: en-us\r\nAccept: */*\r\nSOAPAction: http://tempuri.org/myAction\r\nConnection: keep-alive\r\nUser-Agent: ...\r\n\r\n", webserviceOperationsPath, webserviceHost];//Create the HTTP POST header 
[p_socket writeData:[httpHeader dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:1]; //enqueue the HTTP header
[p_socket writeData:self.p_soapBodyPart1 withTimeout:-1 tag:2]; //enqueue the first part of the SOAP xml
[self setUpStreamsForInputFile: [self.p_mediaURL path]];//set up NSInputStream to read from media file and encode it as Base64

The socket seems to always connect all right, which I see using this delegate method:

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
    NSLog(@"Socket Connected");
}

setUpStreamsForInputFile method (that is called in the first code listing above):

- (void)setUpStreamsForInputFile:(NSString *)inpath {
    self.p_iStream = [[NSInputStream alloc] initWithFileAtPath:inpath];
    [p_iStream setDelegate:self];
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
    dispatch_async(queue, ^ {
        [p_iStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
                           forMode:NSDefaultRunLoopMode];
        [p_iStream open];
        [[NSRunLoop currentRunLoop] run];
    });
}

Now, the NSInputStream setup in the previous method will send events to this delegate:

- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
    switch(eventCode) {
        case NSStreamEventHasBytesAvailable:
        {
            if (stream == self.p_iStream){
                if(!self.p_tempMutableData) {
                    self.p_tempMutableData = [NSMutableData data];
                }
                uint8_t buf[24000];
                unsigned int len = 0;
                len = [p_iStream read:buf maxLength:24000];//read a chunk from the file
                if(len) {
                        [p_tempMutableData appendBytes:(const void *)buf length:len];
                        NSString* base64encData = [Base64 encodeBase64WithData:self.p_tempMutableData];//encode the chunk
                        self.p_streamEncData = [base64encData dataUsingEncoding:NSUTF8StringEncoding];
                        [p_socket writeData:self.p_streamEncData withTimeout:-1 tag:3];//write the encoded chunk to the socket 
                }   
            }
            break;
        }
        case NSStreamEventEndEncountered:
        {
            [stream close];
            [stream removeFromRunLoop:[NSRunLoop currentRunLoop]
                              forMode:NSDefaultRunLoopMode];
            stream = nil;
            [p_socket writeData:self.p_soapBodyPart2 withTimeout:-1 tag:4];//write the second part of SOAP xml
            break;
        }
        ... //some other events handled here
    }
}

The socket is supposed to output things to the log with this delegate

- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
    if (tag == 1)
        NSLog(@"HTTP Header Written");
    else if (tag == 2)
        NSLog(@"Soap Part 1 written");
    else if (tag == 3)
        NSLog(@"File written");
    else if (tag == 4)
        NSLog(@"Soap Part 2 written");
}

but this happens kind of randomly. For example, sometimes I see the first 2 if's called and sometimes not. When I do and it reaches the third "if" (the one where I am writing the actual encoded data), it writes it only 2 or 3 times and that's it - too few times, I think, given the size of the file. I never see it reach the last "if", where it should write the last part of SOAP xml. Would appreciate any help! Thanks in advance.

Further update (3/19/13)

Today testing the socket I am no longer getting the write events at all, which tells me that it is random and I am doing something terribly wrong. Today the connection opens but then times out at some point, as I can see with the following delegate method:

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{    // This method is executed on the socketQueue (not the main thread)
    dispatch_async(dispatch_get_main_queue(), ^{
        @autoreleasepool {
            NSLog(@"socketDidDisconnect:withError: \"%@\"", err);
        }
    });
}

which returns

socketDidDisconnect:withError: "Error Domain=NSPOSIXErrorDomain Code=60 "Operation timed out" UserInfo=0x1cd89b00 {NSLocalizedFailureReason=Error in connect() function, NSLocalizedDescription=Operation timed out}"

while I am still running writes of Base64 data in the stream delegate above.

PeterD
  • 642
  • 1
  • 6
  • 17
  • 1
    Have you tried connecting before starting to write? Also, might be worth implementing socket:didConnectToHost:port: and see if that's called. – Tom Irving Mar 18 '13 at 19:06
  • No, I did not! I now put the connection before the writing and there is a slight progress - still not working but at least moving in the right direction. I will update the question with the result. Thanks! – PeterD Mar 18 '13 at 19:39
  • Reading the comments in the sample GCDAsyncSocket code, it says `Side Note: The AsyncSocket family supports queued reads and writes.This means that you don't have to wait for the socket to connect before issuing your read or write commands.If you do so before the socket is connected, it will simply queue the requests, and process them after the socket is connected.Also, you can issue multiple write commands (or read commands) at a time. You don't have to wait for one write operation to complete before sending another write command`, so, this wasn't the issue... – PeterD Mar 19 '13 at 15:48
  • But then I am not sure again - I moved the connection of the socket to the end of all my writes so that no timeout occurs in the middle, but then in the debugger I can see the write queue of the socket is empty despite my writing to it. So, I do need to connect, I just don't need to *wait* for the connection, I guess. All very confusing... – PeterD Mar 19 '13 at 15:55
  • How about trying reading from the socket, see if the server is sending back any clues about what might be the problem? – Tom Irving Mar 19 '13 at 16:42
  • Yes! Working on it. Before closing hte socket, the server sents this: `HTTP/1.1 400 Bad Request Server: nginx Date: Tue, 19 Mar 2013 16:19:48 GMT Content-Type: text/html Content-Length: 166 Connection: close`. Trying to figure out why... Thank you! – PeterD Mar 19 '13 at 16:47
  • So, the server returns the Bad Request above even before the whole data is written to the socket. I now realize it does not know when the end of the data is gonna come. But I don't know the length of the request in advance b/c I am encoding the Base64 data on the fly. So, I cannot give the server the length of the request. I added `Transfer-Encoding: chunked` to the HTTP header hoping this would force the HTTP 1.1 chunked transfer but this still did not help. – PeterD Mar 19 '13 at 18:55
  • Are you sending the size of each chunk before sending the chunk itself and terminating with a chunk size of 0? – Tom Irving Mar 19 '13 at 19:06
  • Tom, thanks for sticking with me :) I am trying to do that. I am following the specs here http://www.jmarshall.com/easy/http/#http1.1c2 and trying to send first the header, then each of my parts (including the SOAP xml parts) preceded by the HEX representation of the length of the part, then finish it all with a 0 and a blank line. The server throws "Bad request" long before I have a chance to finish though. I keep plugging at it, maybe it'll work in the end. Thanks again. – PeterD Mar 19 '13 at 19:48

0 Answers0