Stream to Get Data - NSInputStream - iphone

All,
I have a server that has a tcp socket stream for communication. I need to get to that stream and read the initial data that it needs to send me.
My current code is as follows. To be honest, I'm going at this completely blind. I'm not sure if this is correct let alone the right thing for the job.
-(void) initNetworkCommunication
{
//input stream
NSInputStream *iStream;
NSURL *url = [url initWithString:#"192.168.17.1:2004"];
[iStream initWithURL:url];
[iStream setDelegate:self];
[iStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[iStream open];
}
So from the way I see it, this code initializes the stream, but how do I read from the stream?
Thanks

There are two ways to get data from a stream: polling and using stream events.
Polling is simpler, but will block the thread it is running in. If you use this method, you don't need to perform the setDelegate: or scheduleInRunLoop:forMode: calls. Polling is performed by repeatedly calling read:maxLength:.
NSInteger result;
uint8_t buffer[BUFFER_LEN]; // BUFFER_LEN can be any positive integer
while((result = [iStream read:buffer maxLength:BUFFER_LEN]) != 0) {
if(result > 0) {
// buffer contains result bytes of data to be handled
} else {
// The stream had an error. You can get an NSError object using [iStream streamError]
}
}
// Either the stream ran out of data or there was an error
Using stream events requires setting the delegate and adding the stream to a run loop. Instead of blocking the thread, the stream will send a stream:handleEvent: message to its delegate when certain events occur, including when it receives data. The delegate can then retrieve the data from the stream. Here is an example stream:handleEvent: method:
- (void)stream:(NSInputStream *)iStream handleEvent:(NSStreamEvent)event {
BOOL shouldClose = NO;
switch(event) {
case NSStreamEventEndEncountered:
shouldClose = YES;
// If all data hasn't been read, fall through to the "has bytes" event
if(![iStream hasBytesAvailable]) break;
case NSStreamEventHasBytesAvailable: ; // We need a semicolon here before we can declare local variables
uint8_t *buffer;
NSUInteger length;
BOOL freeBuffer = NO;
// The stream has data. Try to get its internal buffer instead of creating one
if(![iStream getBuffer:&buffer length:&length]) {
// The stream couldn't provide its internal buffer. We have to make one ourselves
buffer = malloc(BUFFER_LEN * sizeof(uint8_t));
freeBuffer = YES;
NSInteger result = [iStream read:buffer maxLength:BUFFER_LEN];
if(result < 0) {
// error copying to buffer
break;
}
length = result;
}
// length bytes of data in buffer
if(freeBuffer) free(buffer);
break;
case NSStreamEventErrorOccurred:
// some other error
shouldClose = YES;
break;
}
if(shouldClose) [iStream close];
}

I came up with this, based on some other answers.
public enum StreamError: Error {
case Error(error: Error?, partialData: [UInt8])
}
extension InputStream {
public func readData(bufferSize: Int = 1024) throws -> Data {
var buffer = [UInt8](repeating: 0, count: bufferSize)
var data: [UInt8] = []
open()
while true {
let count = read(&buffer, maxLength: buffer.capacity)
guard count >= 0 else {
close()
throw StreamError.Error(error: streamError, partialData: data)
}
guard count != 0 else {
close()
return Data(bytes: data)
}
data.append(contentsOf: (buffer.prefix(count)))
}
}
}

Related

How do I setup CFReadStreamRead to timeout in iOS?

I have legacy code using CFReadStreamRead on the iOS, but if there is no/loss of a connection CFReadStreamRead will block forever. How do I setup CFReadStreamRead to timeout?
Thanks in advance
#Michael Wildermuth Yes, there was an error on opening the stream and got it fixed. Someone who face the same issue, the below code would help.
NSURL *url = [NSURL URLWithString:#"http://www.google.com"];
CFStreamClientContext dataStreamContext = {0, (__bridge void *)(self), NULL, NULL, NULL};
CFHTTPMessageRef message = CFHTTPMessageCreateRequest(kCFAllocatorDefault, CFSTR("GET"), (__bridge CFURLRef)url, kCFHTTPVersion1_1);
NSString *header;
NSDictionary *requestHeaders = [NSDictionary dictionaryWithObject:#"application/html;charset=UTF-8" forKey:#"Content-Type"];
for (header in requestHeaders) {
CFHTTPMessageSetHeaderFieldValue(message, (__bridge CFStringRef)header, (__bridge CFStringRef)[requestHeaders objectForKey:header]);
}
CFHTTPMessageSetBody(message, (CFDataRef)(CFSTR("")));
CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, message);
CFOptionFlags events = kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered;
if(CFReadStreamSetClient(readStream, events, EvenCallBack, &dataStreamContext)){
CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
}
CFReadStreamOpen(readStream);
And for the callback function,
void EvenCallBack(CFReadStreamRef readStream, CFStreamEventType type, void *clientCallBackInfo){
if(CFReadStreamHasBytesAvailable(readStream))
{
uint8_t buf[1024];
unsigned int len = 1024;
CFIndex numBytesRead = CFReadStreamRead(readStream, buf, len);
NSMutableData* data = [[NSMutableData alloc] init];
[data appendBytes:&buf length:numBytesRead];
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"Network read (%d): %#", len, str);
}else {
return;
}
CFHTTPMessageRef message = (CFHTTPMessageRef)CFReadStreamCopyProperty((CFReadStreamRef)readStream, kCFStreamPropertyHTTPResponseHeader);
if (!message) {
NSLog(#"No message");
}
}
Thanks!
I ended up using a home grown timeout. The code was something like this:
uint64_t start = get_current_time();
while(TRUE) {
if(CFReadStreamHasBytesAvailable(stream) == TRUE) {
while ((bytes_read = CFReadStreamRead(stream, buffer, read_size)) > 0) {
// do work!
}
start = get_current_time();
}
uint64_t elapsed = get_current_time() - start;
if(elapsed > timeout) {
break;
}
sleep(10);
}
while ((bytes_read = CFReadStreamRead(stream, buffer, read_size)) > 0) {
// do work!
}
I don't think this code achieves anything special. CFReadStreamRead is internally implemented in a similar manner. Quoting Apple's documentation on this: "This function blocks until at least one byte is available; it does not block until buffer is filled."
I set a custom timeout method:
var hasRecievedUpdate = false
var httpStream: CFReadStream?
var handler: ((Data?, URLResponse?, Error?) -> Void)?
func send() {
let stream = httpStream as Stream
stream.delegate = self
stream.schedule(in: RunLoop.current, forMode: RunLoop.Mode.default)
stream.open()
hasRecievedUpdate = false
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + self.timeout) {
if !self.hasRecievedUpdate
{
stream.close()
self.handler?(nil, nil, nil)
}
}
}
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
self.hasRecievedUpdate = true
...
}

A memory leak happen when doing ftp request in the background thread

I am developing an app which can download image from the server through ftp request and then display them. The ftp request is running in the background. When the download finished, a update message will be sent to the view.
I encountered a memory leak when doing the ftp request which has the following features:
1. The memory leak do not happen every time. May be 1 / 7.
2. If I do the ftp request on the main thread, everything is OK.
3. If I do the ftp request on simulator, everything is OK.
I used SIMPLEFTP to do the ftp job and I have done some modification to fix my request.
In the FtpListService.m, This file is used to request a document list information (name, size, modification date). Memory leak is happened here (I highlight the line with "####").
//This is the method to start a ftp request
- (void)_startReceive
// Starts a connection to download the current URL.
{
BOOL success;
NSURL * url;
CFReadStreamRef ftpStream;
//don't tap receive twice in a row!
assert(self.networkStream == nil);
// First get and check the URL.
self.InputUrl = [self.InputUrl stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
url = [FtpUtil smartURLForString:self.InputUrl];
success = (url != nil);
// If the URL is bogus, let the user know. Otherwise kick off the connection.
if (!success) {
DLog(#"Bad ftp url.");
} else {
// Create the mutable data into which we will receive the listing.
assert(self.listData != nil);
// Open a CFFTPStream for the URL.
ftpStream = CFReadStreamCreateWithFTPURL(NULL, (CFURLRef) url);
assert(ftpStream != NULL);
self.networkStream = (NSInputStream *) ftpStream;
self.networkStream.delegate = self;
[self.networkStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:RUNLOOPMODEL];
//This timer will be called to terminate the request which is blocked for a customed time.
NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:TIMEOUTFTPLIST target:self
selector:#selector(listdealTimeOut:) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:RUNLOOPMODEL];
[self.networkStream open];
CFRelease(ftpStream);
}
}
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
// An NSStream delegate callback that's called when events happen on our
// network stream.
{
connected = #"connected";
switch (eventCode) { //####################### EXC_BAD_ACCESS
case NSStreamEventOpenCompleted: {
//NSLog(#"NSStreamEventOpenCompleted");
} break;
case NSStreamEventHasBytesAvailable: {
NSInteger bytesRead;
uint8_t buffer[LISTDOCBUFFER];
// Pull some data off the network.
bytesRead = [self.networkStream read:buffer maxLength:sizeof(buffer)];
if (bytesRead == -1) {
[self _stopReceiveWithStatus:#"Network read error"];
} else if (bytesRead == 0) {
[self _stopReceiveWithStatus:#"no more data"];
} else {
assert(self.listData != nil);
// Append the data to our listing buffer.
[self.listData appendBytes:buffer length:bytesRead];
[self _parseListData];
}
} break;
case NSStreamEventHasSpaceAvailable: {
//NSLog(#"NSStreamEventHasSpaceAvailable");
assert(NO); // should never happen for the output stream
} break;
case NSStreamEventErrorOccurred: {
DLog(#"NSStreamEventErrorOccurred");
[self _stopReceiveWithStatus:#"Stream open error"];
} break;
case NSStreamEventEndEncountered: {
DLog(#"NSStreamEventEndEncountered");
// ignore
} break;
default: {
DLog(#"default");
assert(NO);
} break;
}
}
In the FtpService.m. Here I counld specify the address and trytime to do ftp request:
- (NSArray *)requstServerListInfo:(NSString *)filename tryTime:(int)tryTime
{
NSArray *result = nil;
//Create the request ftp path
NSString* tm = [NSString stringWithFormat:FTPURL];
if(filename != nil)
tm = [NSString stringWithFormat:#"%#%#/",FTPURL,filename];
while (tryTime-- > 0) {
FtpListService *listService = [[FtpListService alloc] initWithUrl:tm];
[listService _startReceive];
//isReceiving will be NO only when : connect error, time out, correctly done job
//I do not really understand the loop, I just know this will cause the request job to begin
while (listService.isReceiving) {
[[NSRunLoop currentRunLoop] runMode:RUNLOOPMODEL beforeDate:[NSDate distantFuture]];
}
//if correctly request, dirArray != nil
if(listService.dirArray == nil) {
[listService release];
continue;
} else {
result = listService.dirArray;
[listService release];
break;
}
}
return result;
}
The ftp job start from PGNetConductor.m which is a singleton:
pm = [[PGDataManagement alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
result = [pm startNetWork];
}
PGDataManagement is owned by PGNetConductor : #property (nonatomic, unsafe_unretained) PGDataManagement *pm;
I try a lot but failed to solve the problem. Hope someone could give me some advice. If you need the code or more infomation , tell me. Thanks!

CFStream IOS Socket communication

how can i clear an CFStream buffer?
Everytime i read from socket there is still data from an old request, i mean complete response to an old request not just a fragment of it.
Am i missing something ?
This is a function i use to initialize the connection:
-(void)openSocketConnection:(UInt32)port: (NSString *)host
{
NSString *hoststring = [[NSString alloc] initWithString:host];
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault,(__bridge CFStringRef)hoststring ,
port,&_nnet_readStream,&_nnet_writeStream);
CFWriteStreamCanAcceptBytes(_nnet_writeStream);
CFWriteStreamSetProperty(_nnet_writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
CFReadStreamSetProperty(_nnet_readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
if(!CFWriteStreamOpen(_nnet_writeStream)) {
NSLog(#"Error Opening Write Socket");
}
if(!CFReadStreamOpen(_nnet_readStream)) {
NSLog(#"Error Opening Write Socket");
}
}
This is a function i use to read data from socket:
BOOL done = NO;
NSMutableString* result = [NSMutableString string];
while (!done)
{
if (CFReadStreamHasBytesAvailable(_nnet_readStream))
{
UInt8 buf[1024];
CFIndex bytesRead = CFReadStreamRead(_nnet_readStream, buf, 1024);
if (bytesRead < 0)
{
CFStreamError error = CFReadStreamGetError(_nnet_readStream);
NSLog(#"%#",error);
} else if (bytesRead == 0)
{
if (CFReadStreamGetStatus(_nnet_readStream) == kCFStreamStatusAtEnd)
{
done = YES;
}
} else
{
[result appendString:[[NSString alloc] initWithBytes:buf length:bytesRead encoding:NSUTF8StringEncoding]];
}
} else
{
done = YES;
}
}
You seem to assume that when you get data from the host on the other end it will always be available all in one go.
I don't know what the underlying protocol is, but in general it's entirely possible that the remote end will try and send 1600 bytes and that you'll get 1500 bytes but then have to wait a few milliseconds (or even seconds) for the next 100 bytes. Meanwhile CFReadStreamHasBytesAvailable would return false and so your code would set your done flag even though you haven't read all the data the server sent.

Asynchronous IO with CFReadStream

This is a sibling question of Asynchronous IO with CFWriteStream. I'm using CFReadStreamScheduleWithRunLoop and CFReadSteamRead to do asynchronous IO. How can we safely retrieve all the date with blocking?
Let's say the actual size of a message was 1200 (but we don't know), and the size of my read buffer was 1024. A call to CFReadStreamRead will retrieve up to 1024 bytes of data, but since we don't know the size of the message, we should call CFReadStreamRead repeatedly. The problem is that since we don't know how much of data the stream socket has received, the CFReadStreamRead might block from the second call. How can we avoid this problem?
Thanks!
Call again CFReadStreamHasBytesAvailable() on your stream to see if it's still safe to read from it (or if the only way to know is to try).
I've added a sample code, that did the job for me. It's using CFReadStreamHasBytesAvailable(). It's important to check for the status of the stream, otherwise you might end up in an endless loop. My example also includes a timeout handling.
NSMutableData* bodyData = [NSMutableData dataWithCapacity:kHTTPReadStreamBufferSize];
NSDate* startTimeStamp = [NSDate date];
while (TRUE) {
if (CFReadStreamHasBytesAvailable(httpReadStream)) {
startTimeStamp = [NSDate date];
UInt8* streambuffer = malloc(kHTTPReadStreamBufferSize);
int readBytes = CFReadStreamRead (httpReadStream,streambuffer,kHTTPReadStreamBufferSize);
NSLog(#"Read: %d",readBytes);
[bodyData appendBytes:streambuffer length:readBytes];
free(streambuffer);
}
if (CFReadStreamGetStatus(httpReadStream) == kCFStreamStatusError) {
*error = (NSError*)CFReadStreamCopyError (httpReadStream);
if ([*error code] == 61) {
NSLog(#"Error occured: %d",[*error code]);
// connection refused
[PlusError errorForDomainWithCode:kPlusHostUnreachable errorDescription:NSLocalizedString(#"kPlusHostUnreachable",#"")
underlyingError:nil url:nil toError:error];
}
*responseHeader = nil;
*bodyContent = nil;
break;
}
if (CFReadStreamGetStatus(httpReadStream) == kCFStreamStatusAtEnd) {
NSLog(#"Stream reached end!");
*responseHeader = (CFHTTPMessageRef)CFReadStreamCopyProperty(httpReadStream, kCFStreamPropertyHTTPResponseHeader);
*error = nil;
break;
}
// timeout management
NSTimeInterval timeInterval = [startTimeStamp timeIntervalSinceNow]*-1;
if (timeInterval >= kHTTPReadTimeOutTimeInSeconds) {
[PlusError errorForDomainWithCode:kPlusResourceLoadingError errorDescription:NSLocalizedString(#"kPlusResourceLoadingError",#"")
underlyingError:nil url:nil toError:error];
break;
}
}

problem with NSInputStream on real iPhone

I have a problem with NSInputStream. Here is my code:
case NSStreamEventHasBytesAvailable:
printf("BYTE AVAILABLE\n");
int len = 0;
NSMutableData *data = [[NSMutableData alloc] init];
uint8_t buffer[32768];
if(stream == iStream)
{
printf("Receiving...\n");
len = [iStream read:buffer maxLength:32768];
[data appendBytes:buffer length:len];
}
[iStream close];
I try to read small data and it works perfectly on simulator and real iPhone.
If I try to read large data (more than 4kB or maybe 5kB), the real iPhone just can read 2736 bytes and stop.
Why is it? Help me plz!
Merci d'avance!
Your data object needs to be external to your stream handler. It is often the case that when large abounts of data are coming in, you get it in chunks and not all at once. Just keep appending data to it until you receive bytesRead == 0; Then you can close your stream and use the data.
case NSStreamEventHasBytesAvailable: {
NSInteger bytesRead;
uint8_t buffer[32768];
// Pull some data off the network.
bytesRead = [self._networkStream read:buffer maxLength:sizeof(buffer)];
if (bytesRead == -1) {
[self _stopReceiveWithFailure];
} else if (bytesRead == 0) {
[self _stopReceiveWithSuccess];
} else {
[data appendBytes:buffer length:len];
}
Looks like you're creating a new data object every time... perhaps you should be creating & retaining it as a property, and appending to it as you are above.