7.1. Basic Sockets Programming

The most common use of the CFNetwork framework is to communicate across network sockets. A socket is a single channel of communication between two endpoints; think of it as the string connecting two tin cans. Sockets can be established between computers across a network (such as an HTTP connection between the iPhone and a web server), or locally between applications on the same device (such as two programs sharing data). The end responsible for initiating the connection is commonly referred to as the client, while the endpoint that receives and services the connection is considered the server. Another type of paradigm is peer-to-peer networking. Peer-to-peer networks employ ad-hoc connections between standalone client machines to share resources among each other, rather than connecting to a centralized server for the resources. In many situations, however, a central server is used to coordinate peers.

7.1.1. Socket Types

There are two primary types of sockets: UDP and TCP.

A UDP (User Datagram Protocol) socket is used for sending short messages called datagrams to the recipient. Datagrams are single packets of data that are sent and received without any "return postage." There is no guarantee that the recipient will receive a particular packet, and multiple packets may be received out of order. Datagrams are generally thought of as unreliable, in the same way that a carrier pigeon can be unreliable. This form of communication is used for sending short query/response-type messages that do not require authentication, such as DNS (name resolution) lookups, as well as by some protocols where lost packets are irrelevant; such as live video streams and online multiplayer games, where an interruption can be ignored.

The other type of socket, TCP (Transmission Control Protocol), is much more commonly used, as it provides the framework for a complete, structured "conversation" to occur between the two endpoints. TCP connections provide a means to ensure the message was received, and guarantees that packets are received in order. TCP is used by protocols including HTTP, FTP, and others where data must be reliably sent and received in order. In order to keep track of the ordering of packets, TCP employs a sequence number, which identifies the sequence of each packet. This not only keeps your conversation in order, but also adds a reasonable level of protection against some forms of spoofing (packet forgery by a malicious party).

7.1.2. CFSocket

The CFSocket object is the base structure used to create UDP and TCP sockets, and is used to set up, communicate over, and break down a connection. Because the CFNetwork framework is C-based, CFSocket is a structure, rather than a class, and all calls to its functions will include the socket as the first argument.

7.1.2.1. Creating new sockets

You can use the CFSocketCreate function to create both TCP and UDP sockets. This function is provided with the granular information needed to build a socket, including the type of memory allocator to use, protocol family (IPv4 or IPv6), and type of socket. A socket can perform its function in the background of an application by scheduling it on a run loop, so you'll also be able to supply a callback function to be invoked during certain socket events. This allows you to focus on your application, rather than writing your own run loop to wait for connections and data. The prototype for CFSocketCreate follows:

CFSocketRef CFSocketCreate (
    CFAllocatorRef allocator,
    SInt32 protocolFamily,
    SInt32 socketType,
    SInt32 protocol,
    CFOptionFlags callBackTypes,
    CFSocketCallBack callout,
    const CFSocketContext *context
);



CFAllocatorRef allocator

Specifies the type of memory allocator to create the new socket. Use NULL or kCFAllocatorDefault to use the default. Other types of allocators include kCFAllocatorSystemDefault, the system's default allocator (which Apple strongly recommends against using), kCFAllocatorMalloc (which uses malloc(), realloc() and free()), kCFAllocatorMallocZone (which allocates space in unscanned memory), and kCFAllocatorNull (which does not allocate or deallocate memory; useful for cases when data should not be deallocated). You'll almost always want to use the default allocator unless your program calls for a very specific alternative.



SInt32 protocolFamily

The protocol family for the socket. The default is PF_INET (Internet Protocol also known as IPV4). For IPv6 sockets, use PF_INET6.



SInt32 socketType

Identifies the socket type. Use SOCK_STREAM for TCP sockets or SOCK_DGRAM for datagram (UDP) sockets.



SInt32 protocol

The protocol to be used for the socket. This can be one of IPPROTO_TCP or IPPROTO_UDP and should match the socket type specified.



CFOptionFlags callBackTypes

A CFSocket run loop can invoke callbacks for different types of socket events. For example, a callback can be issued when a socket connects, or when there is data available. You construct the callBackTypes argument using a bitmask, which you can set using a bitwise-OR operation. Apple's prototype defines the following enumeration of flags:

enum CFSocketCallBackType {
    kCFSocketNoCallBack = 0,
    kCFSocketReadCallBack = 1,
    kCFSocketAcceptCallBack = 2,
    kCFSocketDataCallBack = 3,
    kCFSocketConnectCallBack = 4,
    kCFSocketWriteCallBack = 8
};
typedef enum CFSocketCallBackType CFSocketCallBackType;



CFSocketCallBack callout

Specifies the function that should be called when one of the events identified in callBackTypes is triggered. Instead of writing your own run loop to wait for connections and send and receive data, using a run loop would instead call this function whenever one of the desired events occurs.



const CFSocketContext *context

A special structure containing a CFSocket's context, which can encapsulate a pointer to your own user-defined data for the socket. You'll learn about this in the next section.



CFTimeInterval timeout

Specifies the time to wait for a connection. If you supply a negative value, the connection will be established in the background, allowing your program to continue running. The callback will then be invoked when the connection is made.

7.1.2.2. Creating sockets from existing sockets

You can create a CFSocket object from an existing native socket by using the CFSocketCreateWithNative function. This function is very similar to the CFSocketCreate function, but accepts the existing socket as one of its arguments. This may be useful in cases where you have legacy code to build a socket, and want to plug it into the CFNetwork framework. The prototype follows:

CFSocketCreateWithNative (
   CFAllocatorRef allocator,
   CFSocketNativeHandle sock,
   CFOptionFlags callBackTypes,
   CFSocketCallBack callout,
   const CFSocketContext *context
);

In the example above, you'll immediately notice that the native socket argument isn't defined as an int, which is the standard on most systems, but rather a CFSocketNativeHandle. The CFSocketNativeHandle data type is defined as the operating system's native data type for C-based sockets, so this will typically resolve to an int on most systems.

7.1.2.3. Socket functions

Once you have created the socket, you can perform a number of functions on it. You can use functions that are specific to the CFNetwork framework, and with a special function named CFSocketGetNative, you can also operate on the socket's lower-level native socket to perform native C-socket functions. The following relevant functions are available on CFSocket objects:



CFSocketGetNative

Returns the system's native socket, on which you can perform the native set of C-based socket operations. This is usually an int data type on most systems. In the example to follow, you'll see it call the native setsockopt() function. This allows you to plug in the CFNetwork framework without sacrificing any functionality, while maintaining compatibility with legacy code.



CFSocketConnectToAddress

Invokes a connect request on the local socket. This is used to connect the socket to a listening (server) socket, such as a web server.



CFSocketCopyAddress

Returns the local address of the CFSocket. This is useful in determining what IP address or addresses your socket is listening on.



CFSocketCopyPeerAddress

Returns the address of the remote socket that the CFSocket is connected to. This provides the IP address of the remote end of the connection, for events when your application is acting as the server.



CFSocketCreateRunLoopSource

Creates a run loop source object for a CFSocket object. You'll see how this works in the example to follow.

7.1.2.4. Enabling/disabling callbacks

Callbacks for CFSocket objects can be enabled or disabled at the programmer's discretion. This is useful in cases where the callback behavior for a socket changes depending on the socket's status. You can do this using the CFSocketDisableCallBacks and CFSocketEnableCallBacks functions.

Both functions accept a socket and a bitwise-OR'd set of callback flags as arguments, matching the callback flags you've already learned. To disable accept and read callbacks for a given socket, your code might look like the example below:

CFSocketDisableCallBacks(mySocket,
    kCFSocketAcceptCallBack | kCFSocketReadCallback);

To reenable them, simply swap the function name to CFSocketEnableCallBacks, as shown below:

CFSocketEnableCallBacks(mySocket,
    kCFSocketAcceptCallBack | kCFSocketReadCallback);

7.1.2.5. Sending data

The CFNetwork framework provides an abstracted routine for sending data, which helps simplify the process. To send data, use the CFSocketSendData command:

char joke[] = "Why did the chicken cross the road?";
kCFSocketError err = CFSocketSendData(mySocket, joke, (strlen(joke)+1), 10);

                                          

You can then check the error code to determine if data was sent successfully:

if (err == kCFSocketSuccess) {
    /* Success */
}

The CFNetwork framework uses the following error codes:



kCFSocketSuccess

Operation succeeded



kCFSocketError

Operation failed



kCFSocketTimeout

Operation timed out

7.1.2.6. Callbacks

You can set certain events to trigger callbacks, such as incoming data or new connections. This allows you to write software that doesn't need to block or loop itself to check the status of the socket. CFNetwork uses a standard callback form factor for all callback functions, providing whatever data is relevant to the type of callback. The prototype for this function follows:

typedef void (*CFSocketCallBack) (
   CFSocketRef s,
   CFSocketCallBackType callbackType,
   CFDataRef address,
   const void *data,
   void *info
);

The following information is provided with each callback. Some information may differ, depending on the type of callback being sent.



CFSocketRef s

The CFSocket corresponding to the event that occurred. This allows your callback function to support multiple sockets.



CFSocketCallBackType callbackType

The enumerated value for the callback, identifying what kind of event has occurred. See the list of callback types from earlier.



CFDataRef address

A CFData object containing the lower-level sockaddr information. You can use this to obtain the remote address to which the socket is connected. This is only provided during accept and data callbacks.



const void *data

A pointer to special data that is relevant to the callback. For a data event, a CFData object is passed containing the received data. For an accept event, a pointer to a CFSocketNativeHandle is provided, pointing to the native socket object. For a connect event, a pointer to an SInt32 error code will be provided.



void *info

The pointer supplied to the CFSocketContext structure associated with the socket. This will contain any user-defined data you've associated with the socket.

7.1.3. CFSocketContext

Because CFSockets can run in the background in a run loop, keeping track of the data that is associated with each connection can become tricky. For example, if you are writing a search engine, you might create hundreds of connections to various web servers, and will need to know which connection is associated with which callback events. The CFSocketContext object allows you to tie a pointer to any such proprietary information to a socket structure so that it is available whenever a callback is triggered.

The context structure prototype follows:

struct CFSocketContext {
    CFIndex version;
    void *info;
    CFAllocatorRetainCallBack retain;
    CFAllocatorReleaseCallBack release;
    CFAllocatorCopyDescriptionCallBack copyDescription;
};
typedef struct CFSocketContext CFSocketContext;



CFIndex version

The version number of the structure. Apple insists this be set to 0.



void *info

A pointer to your application's user-defined data, which will be associated with the CFSocket object when it is created. This pointer will be passed in the arguments list of all callbacks issued by the CFSocket object.



CFAllocatorRetainCallBack retain

An optional callback used when the context is retained. Use NULL if no callback is necessary.



CFAllocatorReleaseCallBack release

An optional callback used when the context is released. Use NULL if no callback is necessary.



CFAllocatorCopyDescriptionCallBack copyDescription

An optional callback invoked when the object is copied into another context. Use NULL if no callback is necessary.

An example socket context follows:

char joke[] = "Why did the chicken cross the road?";
CFSocketContext CTX = { 0, joke, NULL, NULL, NULL };

When operating on a socket, you can obtain the socket's context by calling CFSocketGetContext. This function copies the contents of the socket's context into a local structure provided by the caller:

CFSocketContext localCTX;
CFSocketGetContext(mySocket, &localCTX);

7.1.4. Socket Streams

Socket streams provide an easy interface for reading and writing data to or from a socket. Each socket can be bound to a read and write stream, allowing for synchronous or asynchronous communication. Streams encapsulate most of the work needed for reading and writing byte streams, and replace the traditional error codes send() and recv() functions used in C. Two different stream objects are used with sockets: CFReadStream and CFWriteStream.

7.1.4.1. Read streams

A special set of CFReadStream functions allow for simple read operations on a socket. A read buffer is used, much like in C, in which the read stream is looped until the desired number of bytes are read:

char buffer[1024];
CFIndex bytes_recvd;
int recv_len = 0;

memset(buffer, 0, sizeof(buffer));
while (!strchr(buffer, '\n') && recv_len < sizeof(buffer)) {
    bytes_recvd = CFReadStreamRead(readStream, buffer + recv_len,
        sizeof(buffer) -; recv_len);
    if (bytes_recvd < 0) {
        /* Error has occurred. Close the socket and return. */
    }
    recv_len += bytes_recvd;
}

A list of useful CFReadStream functions follows:



CFReadStreamOpen, CFReadStreamClose

Opens and closes a read stream. These functions allocate and release the resources needed to perform stream reads. A stream must first be opened before it can be attached to a socket.



CFReadStreamRead

The actual read function of the read stream. This function performs reading from the attached socket into the provided buffer, returning the number of bytes that have been read. Much like traditional sockets, the read can be looped until the desired number of bytes has been received.



CFReadStreamGetBuffer

Returns a pointer to the read stream's internal buffer of unread data, allowing the implementer to access the raw buffer directly.



CFReadStreamGetStatus

Returns the current status of the read stream. The status will be one of the following:

  • kCFStreamStatusNotOpen (read stream is not open)

  • kCFStreamStatusOpening (read stream is being opened for reading)

  • kCFStreamStatusOpen (read stream is open and ready)

  • kCFStreamReading (the stream is currently being read from)

  • kCFStreamStatusAtEnd (no more data is available to read from the stream)

  • kCFStreamStatusClosed (the readIin stream has been closed)

  • kFStreamStatusError (an error has occurred on the read stream)



CFReadStreamHasBytesAvailable

Returns a Boolean value indicating whether incoming data is ready to be read without blocking. You can use this to periodically poll the socket about whether data is available, although this will be unnecessary if you are using a run loop.



CFReadStreamScheduleWithRunLoop, CFReadStreamUnscheduleFromRunLoop

Used to schedule or unschedule the stream into or from a run loop. Once scheduled, the loop client is called during certain events, such as opening, errors, and when data is available to be read. You can also schedule a single stream with multiple run loops and modes.



CFReadStreamSetClient

Assigns a client to receive callback for the stream while in the run loop.

If a socket already exists, the CFStreamCreatePairWithSocket function can automatically initialize a read and write stream:

/* The native socket, used for various operations */
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;

CFSocketNativeHandle sock = *(CFSocketNativeHandle *) data;
CFStreamCreatePairWithSocket(kCFAllocatorDefault, sock,
&readStream, &writeStream);

To schedule a read stream onto the run loop, call its scheduler function. This will cause the actual operation of the read stream to be threaded into the background, and will activate its callback functions:

CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(),
        kCFRunLoopCommonModes);

When a read stream has been entered into a run loop, the client's callback functions are called whenever certain events occur:

typedef void (*CFReadStreamClientCallBack) (
   CFReadStreamRef stream,
   CFStreamEventType eventType,
   void *clientCallBackInfo
);

Similar to the CFSocket callback function, the stream callbacks provide information about the type of event and relevant data for the event. You can invoke a callback for the following events:



kCFStreamEventNone

Undefined event.



kCFStreamEventOpenCompleted

The stream has been successfully opened and is ready for further operations.



kCStreamEventHasBytesAvailable

Data is available for the stream to read.



kCFStreamEventErrorOccurred

An error has occurred on the read stream.



kCFStreamEventEndEncountered

An EOF (end-of-file) has been reached, and there is no more data available on the stream.

7.1.4.2. Write streams

The complement to a read stream is a CFWriteStream. This stream is designated for writing, and manages the sending of data through a CFSocket. An example follows:

char data[] = "Beware the Jabberwocky.\n";
CFIndex bytes_sent = 0;
int send_len = 0;

if (CFWriteStreamCanAcceptBytes(writeStream)) {
    bytes_sent = CFWriteStreamWrite(writeStream, data + send_len,
        (strlen(data)+1) - send_len);
    if (bytes_sent < 0) {
        /* Send error occurred. Close the socket and return. */
    }
    send_len += bytes_sent;
}

A list of useful CFWriteStream functions follows:



CFWriteStreamOpen, CFWriteStreamClose

Opens and closes a write stream. These functions allocate and release the resources needed to perform stream writes. A stream must first be opened before it can be attached to a socket.



CFWriteStreamWrite

The actual write function for the write stream. This function performs the writing from a data source to the socket it is paired with, returning the number of bytes that have been sent. Much like traditional send functions performed on C sockets, the write can be looped until the desired number of bytes has been sent.



CFWriteStreamGetStatus

Returns the current status of the write stream. The status will be one of the following:

  • kCFStreamStatusNotOpen (write stream is not open)

  • kCFStreamStatusOpening (write stream is being opened for writing)

  • kCFStreamStatusOpen (write stream is open and ready)

  • kCFStreamWriting (the stream is currently being written to)

  • kCFStreamStatusAtEnd (no more data can be written to the stream)

  • kCFStreamStatusClosed (the write stream has been closed)

  • kFStreamStatusError (an error has occurred on the write stream)



CFReadStreamCanAcceptBytes

Returns a Boolean value indicating whether the stream can be written to.



CFWriteStreamSheduleWithRunLoop, CFWriteStreamUnscheduleFromRunLoop

Used to schedule or unschedule the stream into or from a run loop. After scheduling, the loop client is called during certain events, such as opening, errors, and when data is written. You can also schedule a single stream with multiple run loops and modes.



CFWriteStreamSetClient

Assigns a client to receive callback for the stream while in the run loop.

To schedule a write stream onto the run loop, call its scheduler function. This will cause the actual operation of the write stream to be threaded into the background, and will activate its callback functions:

CFWriteStreamScheduleWithRunLoop(writeStream, CFRunLoopGetCurrent(),
        kCFRunLoopCommonModes);

When a write stream has been entered into a run loop, the client's callback functions are called whenever certain events occur:

typedef void (*CFWriteStreamClientCallBack) (
   CFWriteStreamRef stream,
   CFStreamEventType eventType,
   void *clientCallBackInfo
);

Similar to the CFSocket callback function, the stream callbacks provide information about the type of event and relevant data for the event. A callback may be invoked for the following events:



kCFStreamEventNone

Undefined event.



kCFStreamEventOpenCompleted

The stream has been successfully opened and is ready for further operations.



kCStreamEventCanAcceptBytes

The stream is ready for writing.



kCFStreamEventErrorOccurred

An error has occurred on the read stream.



kCFStreamEventEndEncountered

An EOF (end-of-file) has been reached, and there is no more data available on the stream.

7.1.5. CFSocket Example: Joke Server

In this example, you'll put together your knowledge of the CFSocket and CFSocketCon⁠text structures, as well as streams to build a TCP server program that will tell the punch line to any joke asked of it. You'll store the joke's punch line in the CFSocketContext on the server side and provide the answer to the client. You'll notice an eerie similarity to standard C sockets programming, however the server example will show off the CFNetwork framework's run loop capabilities, which make socket management much easier.

The following code sets up the server socket. Comments have been provided inline:

/* The server socket */
CFSocketRef TCPServer;
#define PORT 2048

    /* The punchline to our joke */
char punchline[] = "To get to the other side!";

    /* Used by setsockopt */
int yes = 1;

    /* Build our socket context; this ties the punchline to the socket */
CFSocketContext CTX = {0, punchline, NULL, NULL, NULL};

    /* Create the server socket as a TCP IPv4 socket and set a callback */
    /* for calls to the socket's lower-level accept() function */
TCPServer = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP,
    kCFSocketAcceptCallBack, (CFSocketCallBack)&AcceptCallback, &CTX);
if (TCPServer == NULL)
    return NULL;

    /* Re-use local addresses, if they're still in TIME_WAIT */
setsockopt(CFSocketGetNative(TCPSocket), SOL_SOCKET, SO_REUSEADDR,
    (void *)&yes, sizeof(yes));

/* Set the port and address we want to listen on */
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_len = sizeof(addr);
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);

NSData *address = [ NSData dataWithBytes: &addr, length:sizeof(addr) ];
if (CFSocketSetAddress(TCPServer, (CFDataRef)address) != kCFSocketSuccess) {
    fprintf(stderr, "CFSocketSetAddress() failed\n");
    CFRelease(TCPServer);
    return NULL;
}

CFRunLoopSourceRef sourceRef =
    CFSocketCreateRunLoopSource(kCFAllocatorDefault, TCPServer, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), sourceRef, kCFRunLoopCommonModes);
CFRelease(sourceRef);

    /* Get on with our life, instead of waiting for incoming connections. */

                                          

Our listening socket is now set up, and a callback function will be invoked whenever a new connection is accepted. The callback function below is one example of how an incoming connection might be handled. In this example, the callback function creates and pairs a read and write stream to the socket. It then waits for the other end to tell a joke, then sends the punch line:

static void AcceptCallBack(CFSocketRef socket,
    CFSocketCallBackType type,
    CFDataRef address,
    const void *data,
    void *info)
{
    CFReadStreamRef readStream = NULL;
    CFWriteStreamRef writeStream = NULL;
    CFIndex bytes;
    UInt8 buffer[128];
    UInt8 recv_len = 0, send_len = 0;

    /* The native socket, used for various operations */
    CFSocketNativeHandle sock = *(CFSocketNativeHandle *) data;

    /* The punch line we stored in the socket context */
    char *punchline = info;

    /* Create the read and write streams for the socket */
    CFStreamCreatePairWithSocket(kCFAllocatorDefault, sock,
        &readStream, &writeStream);

    if (!readStream || !writeSream) {
        close(sock);
        fprintf(stderr, "CFStreamCreatePairWithSocket() failed\n");
        return;
    }

    /* Wait for the client to finish sending the joke; wait for newline */
    memset(buffer, 0, sizeof(buffer));
    while (!strchr(buffer, '\n') && recv_len < sizeof(buffer)) {
        bytes = CFReadStreamRead(readStream, buffer + recv_len,
            sizeof(buffer)-recv_len);
        if (bytes < 0) {
            fprintf(stderr, "CFReadStreamRead() failed\n");
            close(sock);
            return;
        }
        recv_len += bytes;
    }

    /* Send the punchline */
    while (send_len < (strlen(punchline+1))) {
    if (CFWriteStreamCanAcceptBytes(writeStream)) {
        bytes = CFWriteStreamWrite(writeStream,
            punchline + send_len,
            (strlen(punchline)+1) - send_len);
        if (bytes < 0) {
            fprintf(stderr, "CFWriteStreamWrite() failed\n");
            close(sock);
            return;
        }
        send_len += bytes;
    }
    close(sock);
    return;
}

                                          

7.1.6. Further Study

  • Have a look at CFSocket.h, CFStream.h, and CFSocketContext.h. You'll find these deep within your SDK's Core Foundation headers in /Developer/Platforms/iPho⁠neOS.platform.