Networking in Cocoa

Discussion in 'Mac Programming' started by AsmCoder8088, Jul 18, 2008.

  1. AsmCoder8088 macrumors newbie

    Joined:
    Jul 18, 2008
    #1
    I am beginning the process of writing a network-enabled program for OS X using Cocoa and would like to hear your opinions on what would be the best approach to take.

    I know about CFSocket, but would prefer to use non-blocking BSD sockets.

    My problem is that I need to have a way to respond to user-interface events while at the same time checking to see if data arrives on the socket. This is typically done using a worker thread that, in my case, would use the select(...) function to determine if there is any data to be read from the socket. In this way, the worker thread is then woken up whenever the server sends data to the client. So far, so good.

    However, let's suppose the user wants to send a file. This cannot be done in the main thread, as it would block. So we must instead use a worker thread to handling the transmission. Why not just use the other worker thread that is already waiting for data to arrive? Well, as it turns out, select will block until there is data, and we don't want to have to wait for data in order to start the upload. I know -- select can make use of a timeval struct, so it can time out, but then, you'd have the select call timeout after no data is available to be read, and then you'd have to check and see if the user actually issued a command that required sending data. If he or she did not, then the loop would continue, and eat up 100% CPU. Do you see the problem? How can I use just one thread to monitor for incoming data, and at the same time use it to send data based on an event received from the user?

    I guess what I'm trying to do is emulate the asynchronous behavior that CFSocket would provide, but do so using BSD sockets.

    I came up with an idea that would use not just one worker thread, but two. So you have one thread that is solely responsible for handling incoming data, and another that is responsible for taking the button click from a user and initiating a file upload, which would ordinarily block the main thread.

    Does any of this sound reasonable? Am I not thinking of the "obvious" solution? I have spent many, many hours thinking about the best approach. It seems like in networking, there are so many wrong ways to design the software, and only one really good way, but finding the right approach really takes a lot of effort.

    I think this idea would work... It's only downside is that it uses two threads to handle I/O on a socket, and lots of people complain that threads are nasty and should be avoided if at all possible. So, what do I do? I really want to use BSD sockets, but it almost seems impossible to get the kind of asynchronous behavior I need in order to accommodate the event-driven user interface.

    Any ideas or suggestions are very much appreciated. Thanks.
     
  2. AsmCoder8088 thread starter macrumors newbie

    Joined:
    Jul 18, 2008
    #2
    Better solution

    Okay, so I think I've come up with a better solution that uses only one thread, and does not chew up 100% CPU. Here it is in case any one wants it:

    Code:
    // Enter the socket event handling loop
    while ([self GetStateThread] == STATE_THREAD_RUNNING)
    {
    	// Lock the buffer, unless this thread has already done that
    	if (bHasLockedTheSendBuffer == false)
    	{
    		// Has not yet locked the buffer, do so now
    		// This will block Thread.Client if the buffer is locked by another thread.  Which is OK as we expect
    		// the other thread to unlock the buffer as soon as it is done filling it
    		[m_pBufSend LockBuffer];
    		
    		// Update lock status
    		bHasLockedTheSendBuffer = true;
    	}
    	
    	// Determine if the send buffer has any data that should be sent
    	if ([m_pBufSend GetNumBytesRemaining] > 0)
    	{
    		// There is data to send.  Block until it can be written to the socket
    		FD_ZERO(&m_fdSetWrite);
    		FD_SET(m_socket, &m_fdSetWrite);
    		
    		nResult = select(m_socket + 1, NULL, &m_fdSetWrite, NULL, NULL);
    		
    		if (nResult == -1 || nResult == 0)
    		{
    			// An error occurred during select.  We must exit
    			break;
    		}
    		
    		if (!FD_ISSET(m_socket, &m_fdSetWrite))
    		{
    			// Socket should be marked as writable at this point
    			// Interpret this as an error
    			break;
    		}
    
    		// We may write to the socket now
    		[self OnWrite];
    		
    		// Did we get disconnected?
    		if (m_socket == -1)
    		{
    			// Yes
    			break;
    		}
    		
    		// Continue this until all data for Buffer.Send has been sent
    		continue;
    	}
    	
    	// Is there a function that should be called after having sent Buffer.Send?
    	if (m_callbackSend)
    	{
    		// Yes -- call that function so that it may update the send buffer
    		// Remember that we still have a lock on Buffer.Send so this is OK
    		m_callbackSend(self);
    		
    		// Since the callback function merely fills the buffer, and does not send it,
    		// we will need to go back to the top of the loop and send it
    		continue;
    	}
    	
    	// All data has been sent (or there was no data to send).  Unlock the send buffer
    	if (bHasLockedTheSendBuffer == true)
    	{
    		[m_pBufSend UnlockBuffer];
    		
    		// Update lock status
    		bHasLockedTheSendBuffer = false;
    	}
    	
    	// Check to see if we should disconnect (that is, if Main.Thread changed the value of m_bShouldDisconnect)
    	[self LockDisconnectFlag];
    	
    	if (m_bShouldDisconnect)
    	{
    		// Yes, we should disconnect
    		// Reset the flag
    		m_bShouldDisconnect = false;
    		[self UnlockDisconnectFlag];
    		
    		break;
    	}
    	else
    	{
    		// Thread.Main has not issued a disconnect, so we are okay
    		[self UnlockDisconnectFlag];
    	}
    	
    	// Now check to determine if there is data to be received
    	FD_ZERO(&m_fdSetRead);
    	FD_SET(m_socket, &m_fdSetRead);
    	
    	tv.tv_sec = 0;
    	tv.tv_usec = 50000;	// 50 ms to block
    	
    	nResult = select(m_socket + 1, &m_fdSetRead, NULL, NULL, &tv);
    	
    	if (nResult == -1)
    	{
    		// An error occurred during select
    		break;
    	}
    	
    	if (nResult == 0)
    	{
    		continue;
    	}
    	
    	if (!FD_ISSET(m_socket, &m_fdSetRead))
    	{
    		// Socket should be marked as readable
    		// Interpret this as an error
    		break;
    	}
    	
    	// Data can be read from the socket
    	// Lock the send buffer in case it has to be used by a function called from OnRead
    	if (bHasLockedTheSendBuffer == false)
    	{
    		[m_pBufSend LockBuffer];
    		
    		// Update lock status
    		bHasLockedTheSendBuffer = true;
    	}
    	
    	// Check to make sure that there is no data to be sent from the buffer
    	// This could happen between the time we blocked for data to be read and the time
    	// that it timed-out or became available.  In other words, Thread.Main may have
    	// obtained a lock on Buffer.Send, filled it with data, and then released the lock
    	// all during the blocking call to determine if there is data to be read
    	// In that case, if there is data to send, we should go ahead and send it
    	if ([m_pBufSend GetNumBytesRemaining] > 0)
    	{
    		// Yes, there is data to be sent
    		continue;
    	}
    	
    	// No data to be send, and Buffer.Send is locked.  We may proceed to read in data
    	[self OnRead];
    	
    	// Three things may have happened in OnRead.
    	// 1) We read in data, and wrote to a file or displayed a message to the user
    	//    In this case, we don't need to send any data back to the server
    	// 2) We read in data, and the data says to send the server back some information
    	//    If this is the case, Buffer.Send will have bytes remaining, and when we loop
    	//    back to check, we will send the data.
    	//
    	//    In either case, we can maintain the lock on Buffer.Send until we reach the point
    	//    in code where we determine if data can be read from the socket.  Since we wait
    	//    for 50 milliseconds, Thread.Main has plenty of time to obtain a lock if it
    	//    should need to have some data sent
    	// 3) We are disconnected, and so Disconnect was called, which resulted in setting
    	//    m_socket to -1.  Check for this
    	
    	if (m_socket == -1)
    	{
    		// We are disconnected
    		break;
    	}
    }
    
    // Clean up thread and exit
    
    I think this solution should work fairly well.
     

Share This Page