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
...
}
Related
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.
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)))
}
}
}
i want to perform some DNS queries e.g. to get IP records against a specific domain name, i am looking for a preferred way or some useful snippet for this on iOS 3.2+ SDK.
thanx in advance
part from other snippets i found this code
Boolean result;
CFHostRef hostRef;
NSArray *addresses;
NSString *hostname = #"apple.com";
hostRef = CFHostCreateWithName(kCFAllocatorDefault, (CFStringRef)hostname);
if (hostRef) {
result = CFHostStartInfoResolution(hostRef, kCFHostAddresses, NULL); // pass an error instead of NULL here to find out why it failed
if (result == TRUE) {
addresses = (NSArray*)CFHostGetAddressing(hostRef, &result);
}
}
if (result == TRUE) {
[addresses enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSString *strDNS = [NSString stringWithUTF8String:inet_ntoa(*((struct in_addr *)obj))];
NSLog(#"Resolved %d->%#", idx, strDNS);
}];
} else {
NSLog(#"Not resolved");
}
but this is producing same IP for every host Resolved 0->220.120.64.1 any help??
Figured out a change in this snippet makes it working
if (result == TRUE) {
NSMutableArray *tempDNS = [[NSMutableArray alloc] init];
for(int i = 0; i < CFArrayGetCount(addresses); i++){
struct sockaddr_in* remoteAddr;
CFDataRef saData = (CFDataRef)CFArrayGetValueAtIndex(addresses, i);
remoteAddr = (struct sockaddr_in*)CFDataGetBytePtr(saData);
if(remoteAddr != NULL){
// Extract the ip address
//const char *strIP41 = inet_ntoa(remoteAddr->sin_addr);
NSString *strDNS =[NSString stringWithCString:inet_ntoa(remoteAddr->sin_addr) encoding:NSASCIIStringEncoding];
NSLog(#"RESOLVED %d:<%#>", i, strDNS);
[tempDNS addObject:strDNS];
}
}
}
Bros there is a lot simpler way! Thanks to iOS being a unix system, you become a god with unlimited power and resource! I present elegance.
- (NSString*)lookupHostIPAddressForURL:(NSURL*)url
{
// Ask the unix subsytem to query the DNS
struct hostent *remoteHostEnt = gethostbyname([[url host] UTF8String]);
// Get address info from host entry
struct in_addr *remoteInAddr = (struct in_addr *) remoteHostEnt->h_addr_list[0];
// Convert numeric addr to ASCII string
char *sRemoteInAddr = inet_ntoa(*remoteInAddr);
// hostIP
NSString* hostIP = [NSString stringWithUTF8String:sRemoteInAddr];
return hostIP;
}
I am trying to implement async tcp networking with runloop.
currently I manage to connect, but when I try to send something I get that -1 bytes have been written - but CFWriteStreamCopyError returns null.
code sample below, first function connects, second send a simple message.
any help will be appreciated, including random bug spotting (I am new to objective-c and to iphone development in general).
struct header
{
uint32_t length;
uint32_t type;
} header;
- (void) connect
{
NSLog(#"Attempting to (re)connect to %#:%d", m_host, m_port);
while(TRUE)
{
CFHostRef host = CFHostCreateWithName(kCFAllocatorDefault, (CFStringRef)m_host);
if (!host)
{
NSLog(#"Error resolving host %#", m_host);
[NSThread sleepForTimeInterval:5.0];
continue;
}
CFStreamCreatePairWithSocketToCFHost(kCFAllocatorDefault, host , m_port, &m_in, &m_out);
CFRelease(host);
if (!m_in)
{
NSLog(#"Error");
}
CFStreamClientContext context = {0, self,nil,nil,nil};
if (CFReadStreamSetClient(m_in, kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered, networkReadEvent, &context))
{
CFReadStreamScheduleWithRunLoop(m_in, CFRunLoopGetCurrent(),kCFRunLoopCommonModes);
}
if (CFWriteStreamSetClient(m_out, kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered, networkWriteEvent, &context))
{
CFWriteStreamScheduleWithRunLoop(m_out, CFRunLoopGetCurrent(),kCFRunLoopCommonModes);
}
BOOL success = CFReadStreamOpen(m_in);
CFErrorRef error = CFReadStreamCopyError(m_in);
if (!success || (error && CFErrorGetCode(error) != 0))
{
NSLog(#"Connect error %s : %d", CFErrorGetDomain(error), CFErrorGetCode(error));
[NSThread sleepForTimeInterval:5.0];
}
else
{
NSLog(#"Connected");
break;
}
}
[self startSession];
}
- (void) startSession
{
struct header hh;
hh.type = RTR_CREATE_SESSION;
hh.length = 0;
CFIndex res = CFWriteStreamWrite(self.m_out, (const UInt8*)&hh, sizeof(hh));
NSLog(#"Written %d", res);
CFErrorRef error = CFWriteStreamCopyError(self.m_out);
if (error)
{
NSLog(#"Read error %s : %d", CFErrorGetDomain(error), CFErrorGetCode(error));
CFRelease(error);
}
}
figured it out, I forgot to open the write stream as well:
CFWriteStreamOpen(m_out);
Essentially I'm sending data to a Java Socket Server from an iPhone app however something rather strange happens, it doesn't receive the data until the iPhone application is closed! I'm sure there is something I'm missing but I just can't seem to find it, it's all quite odd.
Here is how my connection is created:
-(CFSocketRef)initSocket {
CFSocketContext context = {
.version = 0,
.info = self,
.retain = NULL,
.release = NULL,
.copyDescription = NULL
};
sockety = CFSocketCreate(
kCFAllocatorDefault,
PF_INET,
SOCK_STREAM,
IPPROTO_TCP,
kCFSocketDataCallBack^kCFSocketConnectCallBack,
socketCallBack,
&context
);
uint16_t port = 4444;
struct sockaddr_in addr4;
memset(&addr4, 0, sizeof(addr4));
addr4.sin_family = AF_INET;
addr4.sin_len = sizeof(addr4);
addr4.sin_port = htons(port);
const char *ipaddress = "192.168.1.5";
inet_aton(ipaddress, &addr4.sin_addr);
NSData *address = [NSData dataWithBytes:&addr4 length:sizeof(addr4)];
CFSocketError error = CFSocketConnectToAddress(sockety, (CFDataRef)address, 1);
if(error != kCFSocketSuccess )
{
Faliure = YES;
}
else{
ViewNo = 2;
}
CFRunLoopSourceRef source;
source = CFSocketCreateRunLoopSource(NULL, sockety, 1);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);
return sockety;
}
Heres how the message is sent:
const char *sendStrUTF = [sentmessage UTF8String];
NSData *dataOut = [NSData dataWithBytes: sendStrUTF length: strlen(sendStrUTF)];
CFSocketSendData(sockety, NULL, (CFDataRef) dataOut, 0);
Any help would be greatly appreciated!
Thanks in advance,
Ozzie
Is your call to CFSocketSendData being done on a (blocked) GUI thread?
I would experiment with wrapping those 3 lines in a performSelectorInBackground / or after delay combinations.
NSData *dat=[stringToSend dataUsingEncoding:NSASCIIStringEncoding];
CFSocketError w =CFSocketSendData(_socket, NULL,(CFDataRef)dat, 0);
if (w==kCFSocketSuccess) {
NSLog(#"success");
}