Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UNIX Domain Sockets and Cocoa

I want to implement IPC in a Cocoa application using UNIX domain sockets, with which I have no experience. I found Apple's CFLocalServer example project, but it's written in C and looks, well, fairly complicated (and yes, I have read most of it).

Are the techniques demonstrated in CFLocalServer still state-of-the-art, or is there a way to implement UNIX domain sockets in Objective-C with Cocoa/Foundation?

I'm playing with NSSocketPorts and NSFileHandles (which provide a healthy amount of abstraction, which is great for this project) and found some very-related code in Mike Bean's Networking in Cocoa, but haven't been able to Make It All Work yet.

Anyone done this before?

like image 886
s4y Avatar asked Jun 12 '09 22:06

s4y


People also ask

What are Unix domain sockets used for?

Unix Domain Socket is a way for data communication to occur bi-directionally on the same system. Sockets including Unix Domain Sockets are an important part of Inter-process communication, a beneficial method in which different programs on the same system can communicate with each other.

What is the difference between Unix socket and TCP socket?

A UNIX socket is an inter-process communication mechanism that allows bidirectional data exchange between processes running on the same machine. IP sockets (especially TCP/IP sockets) are a mechanism allowing communication between processes over the network.

Are UNIX domain sockets reliable?

Valid socket types in the UNIX domain are: SOCK_STREAM, for a stream-oriented socket; SOCK_DGRAM, for a datagram-oriented socket that preserves message boundaries (as on most UNIX implementations, UNIX domain datagram sockets are always reliable and don't reorder datagrams); and (since Linux 2.6.

Are UNIX sockets blocking?

The traditional UNIX system calls are blocking. For example: accept() blocks the caller until a connection is present. If no messages space is available at the socket to hold the message to be transmitted, then send() normally blocks.


2 Answers

UNIX Domain Sockets is a tough nut to grab onto. For anyone that has not done so, and is interested then go for it. A sense of accomplishment will ensue afterwards. However, even with the info from Beej and this site or even from Apple there is many disconnects. I am presenting here a conclusive sample for Cocoa that has ARC enabled. I was waiting for Sidnicious and his sample, but never saw anything so I just decided to tackle this myself.

What I have here is a header and .m Implementation file with three interfaces. A superclass interface and then a server and client interface that inherit. I did some limited testing and it seems to work nicely. However, always looking for improvements so please let me know...

Header File:

typedef enum _CommSocketServerStatus {

    CommSocketServerStatusUnknown       = 0,
    CommSocketServerStatusRunning       = 1,
    CommSocketServerStatusStopped       = 2,
    CommSocketServerStatusStarting      = 3,
    CommSocketServerStatusStopping      = 4

} CommSocketServerStatus;

typedef enum _CommSocketClientStatus {

    CommSocketClientStatusUnknown       = 0,
    CommSocketClientStatusLinked        = 1,
    CommSocketClientStatusDisconnected  = 2,
    CommSocketClientStatusLinking       = 3,
    CommSocketClientStatusDisconnecting = 4

} CommSocketClientStatus;

@class CommSocketServer, CommSocketClient;

@protocol CommSocketServerDelegate <NSObject>
@optional
- (void) handleSocketServerStopped:(CommSocketServer *)server;
- (void) handleSocketServerMsgURL:(NSURL *)aURL          fromClient:(CommSocketClient *)client;
- (void) handleSocketServerMsgString:(NSString *)aString fromClient:(CommSocketClient *)client;
- (void) handleSocketServerMsgNumber:(NSNumber *)aNumber fromClient:(CommSocketClient *)client;
- (void) handleSocketServerMsgArray:(NSArray *)aArray    fromClient:(CommSocketClient *)client;
- (void) handleSocketServerMsgDict:(NSDictionary *)aDict fromClient:(CommSocketClient *)client;
@end

@protocol CommSocketClientDelegate <NSObject>
@optional
- (void) handleSocketClientDisconnect:(CommSocketClient *)client;
- (void) handleSocketClientMsgURL:(NSURL *)aURL          client:(CommSocketClient *)client;
- (void) handleSocketClientMsgString:(NSString *)aString client:(CommSocketClient *)client;
- (void) handleSocketClientMsgNumber:(NSNumber *)aNumber client:(CommSocketClient *)client;
- (void) handleSocketClientMsgArray:(NSArray *)aArray    client:(CommSocketClient *)client;
- (void) handleSocketClientMsgDict:(NSDictionary *)aDict client:(CommSocketClient *)client;
@end

@interface CommSocket : NSObject
@property (readonly, nonatomic, getter=isSockRefValid) BOOL sockRefValid;
@property (readonly, nonatomic, getter=isSockConnected) BOOL sockConnected;
@property (readonly, nonatomic) CFSocketRef sockRef;
@property (readonly, strong, nonatomic) NSURL    *sockURL;
@property (readonly, strong, nonatomic) NSData   *sockAddress;
@property (readonly, strong, nonatomic) NSString *sockLastError;
@end

@interface CommSocketServer : CommSocket <CommSocketClientDelegate> { id <CommSocketServerDelegate> delegate; }
@property (readwrite, strong, nonatomic) id delegate;
@property (readonly,  strong, nonatomic) NSSet *sockClients;
@property (readonly, nonatomic) CommSocketServerStatus sockStatus;
@property (readonly, nonatomic) BOOL startServer;
@property (readonly, nonatomic) BOOL stopServer;
- (id) initWithSocketURL:(NSURL *)socketURL;
+ (id) initAndStartServer:(NSURL *)socketURL;
- (void) addConnectedClient:(CFSocketNativeHandle)handle;

- (void) messageClientsURL:(NSURL *)aURL;
- (void) messageClientsString:(NSString *)aString;
- (void) messageClientsNumber:(NSNumber *)aNumber;
- (void) messageClientsArray:(NSArray *)aArray;
- (void) messageClientsDict:(NSDictionary *)aDict;

@end

@interface CommSocketClient : CommSocket { id <CommSocketClientDelegate> delegate; }
@property (readwrite, strong, nonatomic) id delegate;
@property (readonly, nonatomic) CommSocketClientStatus sockStatus;
@property (readonly, nonatomic) CFRunLoopSourceRef sockRLSourceRef;
@property (readonly, nonatomic) BOOL startClient;
@property (readonly, nonatomic) BOOL stopClient;
- (id) initWithSocketURL:(NSURL *)socketURL;
- (id) initWithSocket:(CFSocketNativeHandle)handle;
+ (id) initAndStartClient:(NSURL *)socketURL;
+ (id) initWithSocket:(CFSocketNativeHandle)handle;

- (void) messageReceived:(NSData *)data;
- (BOOL) messageURL:(NSURL *)aURL;
- (BOOL) messageString:(NSString *)aString;
- (BOOL) messageNumber:(NSNumber *)aNumber;
- (BOOL) messageArray:(NSArray *)aArray;
- (BOOL) messageDict:(NSDictionary *)aDict;

@end

Implementation File: (I will present in three sections)

Section I (Superclass)

#import "CommSocket.h"

#import <sys/un.h>
#import <sys/socket.h> 

#pragma mark Socket Superclass:

@interface CommSocket ()
@property (readwrite, nonatomic) CFSocketRef sockRef;
@property (readwrite, strong, nonatomic) NSURL *sockURL;
@end

@implementation CommSocket
@synthesize sockConnected;
@synthesize sockRef, sockURL;

- (BOOL) isSockRefValid {
    if ( self.sockRef == nil ) return NO;
    return (BOOL)CFSocketIsValid( self.sockRef );
}

- (NSData *) sockAddress {

    struct sockaddr_un address;
    address.sun_family = AF_UNIX;
    strcpy( address.sun_path, [[self.sockURL path] fileSystemRepresentation] );
    address.sun_len = SUN_LEN( &address );
    return [NSData dataWithBytes:&address length:sizeof(struct sockaddr_un)];
}

- (NSString *) sockLastError {
    return [NSString stringWithFormat:@"%s (%d)", strerror( errno ), errno ];
}

@end

Section II (The Server)

Note: The server reuses the Client code for clients that connect to itself. OO-programming, gotta love it!

#pragma mark - Socket: Server
#pragma mark -

@interface CommSocketServer ()
@property (readonly, nonatomic) BOOL startServerCleanup;
@property (readwrite, nonatomic) CommSocketServerStatus sockStatus;
@property (readwrite,  strong, nonatomic) NSSet *sockClients;
static void SocketServerCallback (CFSocketRef sock, CFSocketCallBackType type, CFDataRef address, const void *data, void *info);
@end

#pragma mark - Server Implementation:

@implementation CommSocketServer

@synthesize delegate;
@synthesize sockStatus;
@synthesize sockClients;

#pragma mark - Helper Methods:

- (BOOL) socketServerCreate {

    if ( self.sockRef != nil ) return NO;
    CFSocketNativeHandle sock = socket( AF_UNIX, SOCK_STREAM, 0 );
    CFSocketContext context = { 0, (__bridge void *)self, nil, nil, nil };
    CFSocketRef refSock = CFSocketCreateWithNative( nil, sock, kCFSocketAcceptCallBack, SocketServerCallback, &context );

    if ( refSock == nil ) return NO;

    int opt = 1;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
    setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, (void *)&opt, sizeof(opt));

    self.sockRef = refSock;
    CFRelease( refSock );

    return YES;
}

- (BOOL) socketServerBind {
    if ( self.sockRef == nil ) return NO;
    unlink( [[self.sockURL path] fileSystemRepresentation] );
    if ( CFSocketSetAddress(self.sockRef, (__bridge CFDataRef)self.sockAddress) != kCFSocketSuccess ) return NO;
    return YES;
}

#pragma mark - Connected Clients:

- (void) disconnectClients {


    for ( CommSocketClient *client in self.sockClients )
        [client stopClient];

    self.sockClients = [NSSet set];
}

- (void) disconnectClient:(CommSocketClient *)client {

    @synchronized( self ) {
        NSMutableSet *clients = [NSMutableSet setWithSet:self.sockClients];

        if ( [clients containsObject:client] ) {

            if ( client.isSockRefValid ) [client stopClient];
            [clients removeObject:client];
            self.sockClients = clients;
    } }
}

- (void) addConnectedClient:(CFSocketNativeHandle)handle {

    @synchronized( self ) {
        CommSocketClient *client = [CommSocketClient initWithSocket:handle];
        client.delegate = self;
        NSMutableSet *clients = [NSMutableSet setWithSet:self.sockClients];

        if ( client.isSockConnected ) {
            [clients addObject:client];
            self.sockClients = clients;
    } }
}

#pragma mark - Connected Client Protocols:

- (void) handleSocketClientDisconnect:(CommSocketClient *)client {

    [self disconnectClient:client];
}

- (void) handleSocketClientMsgURL:(NSURL *)aURL client:(CommSocketClient *)client {

    if ( [self.delegate respondsToSelector:@selector(handleSocketServerMsgURL:server:fromClient:)] )
        [self.delegate handleSocketServerMsgURL:aURL fromClient:client];
}

- (void) handleSocketClientMsgString:(NSString *)aString client:(CommSocketClient *)client {

    if ( [self.delegate respondsToSelector:@selector(handleSocketServerMsgString:fromClient:)] )
        [self.delegate handleSocketServerMsgString:aString fromClient:client];
}

- (void) handleSocketClientMsgNumber:(NSNumber *)aNumber client:(CommSocketClient *)client {

    if ( [self.delegate respondsToSelector:@selector(handleSocketServerMsgNumber:fromClient:)] )
        [self.delegate handleSocketClientMsgNumber:aNumber client:client];
}

- (void) handleSocketClientMsgArray:(NSArray *)aArray client:(CommSocketClient *)client {

    if ( [self.delegate respondsToSelector:@selector(handleSocketServerMsgArray:fromClient:)] )
        [self.delegate handleSocketServerMsgArray:aArray fromClient:client];
}

- (void) handleSocketClientMsgDict:(NSDictionary *)aDict client:(CommSocketClient *)client {

    if ( [self.delegate respondsToSelector:@selector(handleSocketServerMsgDict:fromClient:)] )
        [self.delegate handleSocketServerMsgDict:aDict fromClient:client];
}

#pragma mark - Connected Client Messaging:

- (void) messageClientsURL:(NSURL *)aURL {
    for ( CommSocketClient *client in self.sockClients)
        [client messageURL:aURL];
}

- (void) messageClientsString:(NSString *)aString {
    for ( CommSocketClient *client in self.sockClients)
        [client messageString:aString];
}

- (void) messageClientsNumber:(NSNumber *)aNumber {
    for ( CommSocketClient *client in self.sockClients)
        [client messageNumber:aNumber];
}

- (void) messageClientsArray:(NSArray *)aArray {
    for ( CommSocketClient *client in self.sockClients)
        [client messageArray:aArray];
}

- (void) messageClientsDict:(NSDictionary *)aDict {
    for ( CommSocketClient *client in self.sockClients)
        [client messageDict:aDict];
}

#pragma mark - Start / Stop Server:

- (BOOL) startServerCleanup { [self stopServer]; return NO; }

- (BOOL) startServer {

    if ( self.sockStatus == CommSocketServerStatusRunning ) return YES;
    self.sockStatus = CommSocketServerStatusStarting;

    if ( ![self socketServerCreate] ) return self.startServerCleanup;
    if ( ![self socketServerBind]   ) return self.startServerCleanup;

    CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource( kCFAllocatorDefault, self.sockRef, 0 );
    CFRunLoopAddSource( CFRunLoopGetCurrent(), sourceRef, kCFRunLoopCommonModes );
    CFRelease( sourceRef );

    self.sockStatus = CommSocketServerStatusRunning;
    return YES;
}

- (BOOL) stopServer {

    self.sockStatus = CommSocketServerStatusStopping;

    [self disconnectClients];

    if ( self.sockRef != nil ) {

        CFSocketInvalidate(self.sockRef);
        self.sockRef = nil;
    }

    unlink( [[self.sockURL path] fileSystemRepresentation] );

    if ( [self.delegate respondsToSelector:@selector(handleSocketServerStopped:)] )
        [self.delegate handleSocketServerStopped:self];

    self.sockStatus = CommSocketServerStatusStopped;
    return YES;
}

#pragma mark - Server Validation:

- (BOOL) isSockConnected {

    if ( self.sockStatus == CommSocketServerStatusRunning )
        return self.isSockRefValid;

    return NO;
}

#pragma mark - Initialization:

+ (id) initAndStartServer:(NSURL *)socketURL {

    CommSocketServer *server = [[CommSocketServer alloc] initWithSocketURL:socketURL];
    [server startServer];
    return server;
}

- (id) initWithSocketURL:(NSURL *)socketURL {

    if ( (self = [super init]) ) {

        self.sockURL     = socketURL;
        self.sockStatus  = CommSocketServerStatusStopped;
        self.sockClients = [NSSet set];

    } return self;
}

- (void) dealloc { [self stopServer]; }

#pragma mark - Server Callback:

static void SocketServerCallback (CFSocketRef sock, CFSocketCallBackType type, CFDataRef address, const void *data, void *info) {

    CommSocketServer *server = (__bridge CommSocketServer *)info;

    if ( kCFSocketAcceptCallBack == type ) {
        CFSocketNativeHandle handle = *(CFSocketNativeHandle *)data;
        [server addConnectedClient:handle];
    }
}

@end

Section III (The Client)

#pragma mark - Socket: Client
#pragma mark -

@interface CommSocketClient ()
@property (readonly, nonatomic) BOOL startClientCleanup;
@property (readwrite, nonatomic) CommSocketClientStatus sockStatus;
@property (readwrite, nonatomic) CFRunLoopSourceRef sockRLSourceRef;
static void SocketClientCallback (CFSocketRef sock, CFSocketCallBackType type, CFDataRef address, const void *data, void *info);
@end

#pragma mark - Client Implementation:

@implementation CommSocketClient

static NSTimeInterval const kCommSocketClientTimeout = 5.0;

@synthesize delegate;
@synthesize sockStatus;
@synthesize sockRLSourceRef;

#pragma mark - Helper Methods:

- (BOOL) socketClientCreate:(CFSocketNativeHandle)sock {

    if ( self.sockRef != nil ) return NO;
    CFSocketContext context = { 0, (__bridge void *)self, nil, nil, nil };
    CFSocketCallBackType types = kCFSocketDataCallBack;
    CFSocketRef refSock = CFSocketCreateWithNative( nil, sock, types, SocketClientCallback, &context );

    if ( refSock == nil ) return NO;

    int opt = 1;
    setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, (void *)&opt, sizeof(opt));

    self.sockRef = refSock;
    CFRelease( refSock );

    return YES;
}

- (BOOL) socketClientBind {
    if ( self.sockRef == nil ) return NO;
    if ( CFSocketConnectToAddress(self.sockRef, 
                                  (__bridge CFDataRef)self.sockAddress, 
                                  (CFTimeInterval)kCommSocketClientTimeout) != kCFSocketSuccess ) return NO;
    return YES;
}

#pragma mark - Client Messaging:

- (void) messageReceived:(NSData *)data {

    id msg = [NSKeyedUnarchiver unarchiveObjectWithData:data];

    if ( [msg isKindOfClass:[NSURL class]] ) {

        if ( [self.delegate respondsToSelector:@selector(handleSocketClientMsgURL:client:)] )
            [self.delegate handleSocketClientMsgURL:(NSURL *)msg client:self];
    }

    else if ( [msg isKindOfClass:[NSString class]] ) {

        if ( [self.delegate respondsToSelector:@selector(handleSocketClientMsgString:client:)] )
            [self.delegate handleSocketClientMsgString:(NSString *)msg client:self];
    }

    else if ( [msg isKindOfClass:[NSNumber class]] ) {

        if ( [self.delegate respondsToSelector:@selector(handleSocketClientMsgNumber:client:)] )
            [self.delegate handleSocketClientMsgNumber:(NSNumber *)msg client:self];
    }

    else if ( [msg isKindOfClass:[NSArray class]] ) {

        if ( [self.delegate respondsToSelector:@selector(handleSocketClientMsgArray:client:)] )
            [self.delegate handleSocketClientMsgArray:(NSArray *)msg client:self];
    }

    else if ( [msg isKindOfClass:[NSDictionary class]] ) {

        if ( [self.delegate respondsToSelector:@selector(handleSocketClientMsgDict:client:)] )
            [self.delegate handleSocketClientMsgDict:(NSDictionary *)msg client:self];
    }
}

- (BOOL) messageData:(NSData *)data {

    if ( self.isSockConnected ) {

        if ( kCFSocketSuccess == CFSocketSendData(self.sockRef, 
                                                  nil, 
                                                  (__bridge CFDataRef)data, 
                                                  kCommSocketClientTimeout) )
            return YES;

    } return NO;
}

- (BOOL) messageURL:(NSURL *)aURL          { return [self messageData:[NSKeyedArchiver archivedDataWithRootObject:aURL]];    }
- (BOOL) messageString:(NSString *)aString { return [self messageData:[NSKeyedArchiver archivedDataWithRootObject:aString]]; }
- (BOOL) messageNumber:(NSNumber *)aNumber { return [self messageData:[NSKeyedArchiver archivedDataWithRootObject:aNumber]]; }
- (BOOL) messageArray:(NSArray *)aArray    { return [self messageData:[NSKeyedArchiver archivedDataWithRootObject:aArray]];  }
- (BOOL) messageDict:(NSDictionary *)aDict { return [self messageData:[NSKeyedArchiver archivedDataWithRootObject:aDict]];   }

#pragma mark - Start / Stop Client:

- (BOOL) startClientCleanup { [self stopClient]; return NO; }

- (BOOL) startClient {

    if ( self.sockStatus == CommSocketClientStatusLinked ) return YES;
    self.sockStatus = CommSocketClientStatusLinking;

    CFSocketNativeHandle sock = socket( AF_UNIX, SOCK_STREAM, 0 );
    if ( ![self socketClientCreate:sock] ) return self.startClientCleanup;
    if ( ![self socketClientBind]        ) return self.startClientCleanup;

    CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource( kCFAllocatorDefault, self.sockRef, 0 );
    CFRunLoopAddSource( CFRunLoopGetCurrent(), sourceRef, kCFRunLoopCommonModes );

    self.sockRLSourceRef = sourceRef;
    CFRelease( sourceRef );

    self.sockStatus = CommSocketClientStatusLinked;
    return YES;
}

- (BOOL) stopClient {

    self.sockStatus = CommSocketClientStatusDisconnecting;

    if ( self.sockRef != nil ) {

        if ( self.sockRLSourceRef != nil ) {

            CFRunLoopSourceInvalidate( self.sockRLSourceRef );
            self.sockRLSourceRef = nil;
        }

        CFSocketInvalidate(self.sockRef);
        self.sockRef = nil;
    }

    if ( [self.delegate respondsToSelector:@selector(handleSocketClientDisconnect:)] )
        [self.delegate handleSocketClientDisconnect:self];

    self.sockStatus = CommSocketClientStatusDisconnected;

    return YES;
}

#pragma mark - Client Validation:

- (BOOL) isSockConnected {

    if ( self.sockStatus == CommSocketClientStatusLinked )
        return self.isSockRefValid;

    return NO;
}

#pragma mark - Initialization:

+ (id) initAndStartClient:(NSURL *)socketURL {

    CommSocketClient *client = [[CommSocketClient alloc] initWithSocketURL:socketURL];
    [client startClient];
    return client;
}

+ (id) initWithSocket:(CFSocketNativeHandle)handle {

    CommSocketClient *client = [[CommSocketClient alloc] initWithSocket:handle];
    return client;
}

- (id) initWithSocketURL:(NSURL *)socketURL {

    if ( (self = [super init]) ) {

        self.sockURL    = socketURL;
        self.sockStatus = CommSocketClientStatusDisconnected;

    } return self;
}

- (id) initWithSocket:(CFSocketNativeHandle)handle {

    if ( (self = [super init]) ) {

        self.sockStatus = CommSocketClientStatusLinking;

        if ( ![self socketClientCreate:handle] ) [self startClientCleanup];

        else {

            CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource( kCFAllocatorDefault, self.sockRef, 0 );
            CFRunLoopAddSource( CFRunLoopGetCurrent(), sourceRef, kCFRunLoopCommonModes );

            self.sockRLSourceRef = sourceRef;
            CFRelease( sourceRef );

            self.sockStatus = CommSocketClientStatusLinked;
        }

    } return self;
}

- (void) dealloc { [self stopClient]; }

#pragma mark - Client Callback:

static void SocketClientCallback (CFSocketRef sock, CFSocketCallBackType type, CFDataRef address, const void *data, void *info) {

    CommSocketClient *client = (__bridge CommSocketClient *)info;

    if ( kCFSocketDataCallBack == type ) {

        NSData *objData = (__bridge NSData *)data;

        if ( [objData length] == 0 )
            [client stopClient];

        else
            [client messageReceived:objData];
    }
}

@end

Ok, that's it and it should work from different processes.

Simply use this to create a server with the path of where to put the socket file.

  • (id) initAndStartServer:(NSURL *)socketURL;

Likewise, use this to create a client connection

  • (id) initAndStartClient:(NSURL *)socketURL;

The rest should be straightforward with the delegate methods I included. Lastly, keep the url path small (didn't add any real checking for this) and you can instantiate these in a separate NSOperationQueue (untested though).

Hope this helps someone out there as a complete working sample. Arvin

like image 160
Arvin Avatar answered Oct 18 '22 03:10

Arvin


In the end, I did use UNIX domain sockets, and they work pretty well. I have launchd set up the socket for my server (but did write code, successfully, to create it myself), and have the client connect to it.

Things I learned:

  • You can wrap both ends of the connection in NSFileHandle

  • You do need to use connect() on the client side to create a connection to the socket

  • You should make both the client and the server ignore SIGPIPE

  • If you get called back with zero-length data, it means that the thing on the other end of the socket has disconnected (i.e. the server/client exited). Make sure to close and release your end of the socket in this case — don't try to read data from it again, or just get another zero-length read callback (for ever and ever)

  • You’re responsible for delimiting and assembling the messages you send across the socket (a single message sent on one end may be spit into more than one on the other end, or multiple messages might be merged)

I would be happy to share or publish code, just haven’t had the time to clean it up for public consumption. If anyone is interested, let me know.

like image 30
s4y Avatar answered Oct 18 '22 03:10

s4y