Background
I have been kind of teaching myself about sockets and network programming from various sample code and the classic Unix Network Programming textbook while simultaneously trying to put the knowledge to work in an app that I am working on right now. I am currently working on a part of the application which requires a simple client-server setup.
Here's how it goes right now (well, how it should go):
- Server publishes itself with
NSNetService
and creates a socket usingCFSocketCreateWithNative()
- Client finds the server with
NSNetServiceBrowser
- Client resolves the discovered service
- Server gets a callback from CFSocket, which creates a new instance of a class (
MyConnection
) to handle the connection. Read and write streams for the connection are attained withCFStreamCreatePairWithSocket()
. - The client messages the server (@"hi")
- The server sends the data that it receives from the client back to the client (this is where my problem is)
- The client displays the string in a UIAlertView
Two Questions
I am getting an "Operation now in progress" error when I try to have the server send the data back to the client, as annotated in the Connection code below. I believe this is because the
NSOutputStream
does not have space available. What is the best way to deal with this? I know it should wait for theNSStreamEventHasSpaceAvailable
event, but it seems that it is not happening…
EDIT: duh... When I was getting this error I was testing the app with the iPhone simulator only and letting it act as the server and client because I didn't have internet in my new apartment yet :P It seems to be a non-issue when using two real devices.Is it possible to make this server so that the sending and receiving of data from each connection object does not block the sending and receiving of data from other connection objects? Does each new connection object need to be on a new runloop or thread or something like that? I have fished around the apple concurrency documentation, but nothing is jumping out... The goal is to have a reply sent to the client as fast as possible, no matter how many other clients are connected to the server.
UPDATE: In stead of allowing concurrent connections to this server I am considering just queueing the connections and handling them one at a time since the amount of data that needs to be sent to each client is very small. Is this the best decision? What if there are hundreds of clients in the queue? On second thought, this might be a bad idea because the establishment of a connection takes a second or two on a fast local network, and takes even longer with bluetooth... I would love some expert advice on this matter :)
Relevant Code
Note: APNetService and APNetServiceBrowser are analogous to NSNetService and NSNetServiceBrowser
Server Code
- (void) startServerForGroup:(NSString *)name
{
self.groupName = name;
NSInteger port = [self prepareListeningSocket];
self.service = [[APNetService alloc] initWithDomain:@"local."
type:@"_example._tcp."
name:self.groupName
port:port];
self.service.delegate = self;
[self.service publish];
}
- (NSInteger) prepareListeningSocket
{
int listenfd, err, junk, port;
BOOL success;
struct sockaddr_in addr;
port = 0;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
success = (listenfd != -1);
if (success) {
bzero(&addr, sizeof(addr));
addr.sin_len = sizeof(addr);
addr.sin_family = AF_INET;
addr.sin_port = 0;
addr.sin_addr.s_addr = INADDR_ANY;
err = bind(listenfd, (const struct sockaddr *) &addr, sizeof(addr));
success = (err == 0);
}
if (success) {
err = listen(listenfd, 5);
success = (err == 0);
}
if (success) {
socklen_t addrLen;
addrLen = sizeof(addr);
err = getsockname(listenfd, (struct sockaddr *) &addr, &addrLen);
success = (err == 0);
if (success) {
assert(addrLen == sizeof(addr));
port = ntohs(addr.sin_port);
}
}
if (success) {
CFSocketContext context = { 0,(__bridge void*) self, NULL, NULL, NULL };
CFSocketRef socket = CFSocketCreateWithNative(
NULL,
listenfd,
kCFSocketAcceptCallBack,
AcceptCallback,
&context
);
if (socket) {
self.listeningSocket = socket;
CFRelease(socket);
success = YES;
}
if (success) {
CFRunLoopSourceRef rls;
listenfd = -1;
rls = CFSocketCreateRunLoopSource(NULL, self.listeningSocket, 0);
assert(rls != NULL);
CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
CFRelease(rls);
}
}
if ( success ) {
return port;
}
else {
NSLog(@"FAILED TO START SERVER");
if (listenfd != -1) {
junk = close(listenfd);
assert(junk == 0);
}
return -1;
}
}
#pragma mark - Callback
// Called by CFSocket when someone connects to the listening socket
static void AcceptCallback(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info)
{
MyServer * obj;
obj = (__bridge MyServer *) info;
assert(s == obj->_listeningSocket);
MyConnection *newCon = [[MyConnection alloc] initWithFileDescriptor:*(int*)data];
[newCon startReceive];
//add the new connection object to the servers mutable array of connections
[obj.connections addObject:newCon];
}
Connection Code
- (void) startReceive
{
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocket(NULL, self.fd, &readStream, &writeStream);
self.inputStream = (__bridge_transfer NSInputStream *) readStream;
self.outputStream = (__bridge_transfer NSOutputStream*) writeStream;
[self.inputStream setProperty:(id)kCFBooleanTrue forKey:(NSString *)kCFStreamPropertyShouldCloseNativeSocket];
[self.outputStream setProperty:(id)kCFBooleanTrue forKey:(NSString *)kCFStreamPropertyShouldCloseNativeSocket];
self.inputStream.delegate = self;
self.outputStream.delegate = self;
[self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self.inputStream open];
[self.outputStream open];
}
#pragma mark - NSStreamDelegate
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
switch (eventCode) {
case NSStreamEventHasBytesAvailable: {
NSInteger bytesRead;
uint8_t buffer[32768];
bytesRead = [self.inputStream read:buffer maxLength:sizeof(buffer)];
if (bytesRead == -1)...
else if (bytesRead == 0)...
else {
NSData *data = [NSData dataWithBytes:buffer length:bytesRead];
[self didReceiveData:data];
}
} break;
case NSStreamEventHasSpaceAvailable: {
self.space = YES;
} break;
. . .
}
}
- (void) didReceiveData:(NSData *)data
{
if (self.space)
NSLog(@"SPACE");
else
NSLog(@"NO SPACE"); //this gets printed
NSInteger i = [self.outputStream write:data.bytes maxLength:data.length];
if (i < 0) {
printf("%s",strerror(errno)); //"Operation now in progress" error
}
}
Client Code
#pragma mark - APNetServiceBrowserDelegate
- (void) browser:(APNetServiceBrowser *)browser didAddService:(APNetService *)service moreComing:(BOOL)moreComing
{
//omitting checks that determine which server to connect to, if multiple
service.delegate = self;
[service resolveWithTimeout:20];
}
#pragma mark - APNetServiceDelegate
- (void) netServiceDidResolveAddress:(APNetService *)service
{
NSInputStream *input;
NSOutputStream *output;
[service getInputStream:&input outputStream:&output];
self.inputStream = input;
self.outputStream = output;
self.inputStream.delegate = self;
self.outputStream.delegate = self;
[self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.inputStream open];
[self.outputStream open];
}
#pragma mark - NSStreamDelegate
- (void) stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
switch (eventCode) {
case NSStreamEventHasBytesAvailable: {
NSInteger bytesRead;
uint8_t buffer[32768];
bytesRead = [self.inputStream read:buffer maxLength:sizeof(buffer)];
if (bytesRead == -1) NSLog(@"Error reading data");
else if (bytesRead == 0) NSLog(@"no bytes read");
else {
NSData *data = [NSData dataWithBytes:buffer length:bytesRead];
[self didReceiveData:data];
}
} break;
case NSStreamEventHasSpaceAvailable: {
if (!self.isWaitingForReply) {
[self sendHelloMessage];
}
} break;
//omitted other NSStreamEvents
}
}
- (void) sendHelloMessage
{
NSData *d = [NSKeyedArchiver archivedDataWithRootObject:@"hi"];
[self.outputStream write:d.bytes maxLength:d.length];
self.isWaiting = YES;
}
- (void) didReceiveData:(NSData *)data
{
NSString *string = [NSKeyedUnarchiver unarchiveObjectWithData:data];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Message"
message:string
delegate:self
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
}