(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.