Developer Area Developer Area
www.be.com

Becoming A Be Developer

Becoming A Be Developer

Join Registered Developer Program

Registered Developer Area

Developer Library

Developer Library

Developer Events

Search Contact


Programming Tutorial: Networking

[Note: This tutorial assumes the reader has an understanding of BeOS programming basics. Other tutorials, especially Approaching Be, can help the beginning programmer. Having a copy of The Be Book nearby will also prove useful. Source code for this tutorial can be found at ftp://ftp.be.com/pub/samples/preview/network_kit/net_tutorial.tgz.]


Introduction

The objective of this tutorial is to step through a simple example of a working tcp/ip client and server. In this case, the application is a "virtual blackboard" which allows users to connect to a server and then draw in a shared window; any drawing done in any user's window also appears on all other users' windows. The only other thing it does is provide a "clear" command on the application menu, which clears all blackboards. For the most part, we will ignore the user interface and focus on the networking parts of the program.

One important note first; there are some problems with DR8 networking. See the "Other Notes" section at the end for more details. However, the socket code used here should compile with no required changes in DR9. (It does so with the current build, at least)

If you're not at least a little familiar with threads and the BeOS' BLooper and BMessage objects, please read the sections on these.

Not on a network? Don't worry! You can use the built-in loopback address, 127.0.0.1, as your address, and everything will be just fine.


Code Organization

The foundation of all the networking in the tutorial is two classes, BTSSocket and BTSAddress. BTSSocket objects represent socket connections, both client and server. BTSAddress objects represent network addresses.

Both the client and the server are constructed in two layers; one layer consists of a generic BMessage-based client and server. The server operates by receiving data from clients, converting it into BMessage objects, and posting the resulting message to whatever the assigned receiver is. To send messages to clients, BMessages are posted to this server object. Messages can be targeted at a specific client or sent to all clients at once. Similarly, the message clients convert data received from the server into BMessages and post them, to an assigned message receiver, and any messages locally posted to a client are sent to the server.

On top of this is the drawing server and the drawing client code. These operate by creating instances of the BMessage server or client, repectively, and specifying themselves as the message recipients.

Here is a summary of the generic classes used in the program.

BTSAddress.cpp       -- Definition of network address object.
BTSSocket.cpp        -- Definition of network socket object. Implements
                        low-level network functions (bind, send, recv,etc)
BTSNetMsgUtils.cpp   -- Utility functions used by both client and server
                        to send and receive BMessages over the network.
BTSNetMsgServer.cpp  -- BMessage-based server definition
BTSServerMonitor.cpp -- Simple server UI representation, allows you to 
                        start a server and displays the number of 
                        connections. Also allows for selection of the
                        loopback.
BTSNetMsgClient.cpp  -- BMessage-based client definition
BTSPrefDlog.cp       -- A generic client interface, that lets you specify what
                        host you want to connect to.

In addition to these, the following classes exist to create the draw server and client:

BTSNetDrawServer.cpp -- Creates an instance of BTSNetMsgServer and manages it.
BTSNetDrawClient.cpp -- Creates an instance of BTSNetMsgClient and manages it.
BTSNetDrawWindow.cpp -- Client drawing window.
BTSNetDrawView.cpp   -- Client drawing view.


Making a Server

First, let's examine the generic message server.

The server is a BLooper, which means you can communicate with it by posting messages to it. Threads are only used where necessary to avoid blocking on network functions (such as accept()). There is only one thread to manage all incoming client data, and outgoing data is serialized through the MessageReceived() method of the message server. There are 3 threads total in the messaging server:

  1. Created by subclassing the server from BLooper. This is the one we post to when we want to send a message to a client or clients.

  2. Connection handler. Continuously waits for client connection requests.

  3. Client listener. Continuously waits for incoming client data.

The drawing server also creates a BApplication and BWindow thread, as usual.

Preparing to start the server

Servers operate by attaching themselves to a certain port number, then waiting for clients to connect. Clients must have prior knowledge of the port number (or the service name, which is just an alias to a port number) so that they can connect to the server.

In order for a server to be officially attached, it has to jump through several hoops. This is encapsulated in the BTSNetMsgServer::Run() method. The server requires the following information before it can attach to a port:

port     -- number used as rendezvous point for clients and servers.
address  -- address of network interface to use for server (there may
            be multiple interfaces). There are problems in DR8 with
            attaching to multiple interfaces simultaneously. See
            The Be Book for more details.
family   -- socket's network address format. Currently must be AF_INET.
type     -- either SOCK_STREAM or SOCK_DGRAM, for stream or datagram sockets.
            (TCP/IP is a stream socket).
protocol -- "Messaging" protocol for socket. If you set it to 0,
            it will pick the "natural" one for the socket type; for 
            SOCK_STREAM, this is IPPROTO_TCP.

So, this stuff says "I want to connect on this port using this network interface, communicating in this way.". For the purposes of this app, the communication is always TCP/IP.

At a low-level, the steps required to connect using this information are:

  1. Create a socket using socket().
  2. Bind the socket to an address using bind().
  3. Start listening for clients by calling listen().

The specifics of doing these things are hidden inside of the BTSSocket class; They are found, respectively, in the constructor and the methods Bind() and Listen(). If you'd l ike to see details on this, check out the code now and the discussions in the Network Kit.

The BTSSocket object used by the server for accepting client connections is created in the constructor for BTSNetMsgServer:

BTSNetMsgServer::BTSNetMsgServer(const unsigned short port, 
                        BLooper* messageReceiver,
                        const long priority, 
                        const int maxConnections, 
                        const unsigned long address, 
                        const int family, 
                        const int type, 
                        const int protocol  ) :
                        fSocket(type,protocol, family),
                        fAddress(family,port, address),
                        fPriority(priority),
                        fMaxConnections(maxConnections)
{
    
    if (messageReceiver == NULL)
    {
        messageReceiver = be_app;
    }
    fIsExiting = FALSE;
    fSocketListSem = ::create_sem(1, "Client Socket List Sem");
    
    ::acquire_sem(fSocketListSem);
    fMessageReceiver = messageReceiver;
    return;
}

A BTSAddress object is also created; this is just a convenience class that encapsulates family, port, and hostname, and does some conversions, like giving us the host entry of the machine the address is associated with. The socket created is the socket the server will listen for clients on.

Now that we have a socket, we have to bind and start listening for clients. This is done when the Server's thread is started by the Run() method:

thread_id BTSNetMsgServer::Run() { thread_id theID = -1; // To return thread id of this thread int result = 0; // Results of socket function calls // Bind the socket to the port/address specified in sockAddr result = fSocket.BindTo(fAddress); if (result >= 0) // Was bind successful? { // Start listening for connections. result = fSocket.Listen(fMaxConnections);

Assuming the server has a valid socket, the socket is bound to the network address that clients will communicate with it on. Then Listen() is called to start listening for clients, and is passed the maximum number of connections the server can handle.

The remaining steps are to accept clients and receive client data. However, if no clients are connecting or no clients are sending data to the server, attempts to accept clients or receive data will block. For this reason, the server spawns one thread for each of these. Continuing on in BTSNetMsgServer::Run():

        if (result >= 0)
        {
            //Start the main server thread.
            theID = BLooper::Run();
            if (theID > B_NO_ERROR)                // Did server thread start?
            {
                // Start separate thread to handle conn. requests.
                fConnectionRequestHandlerID = 
                ::spawn_thread(HandleConnectionRequests, 
                            kConnectionRequestHandlerName, 
                            fPriority, 
                            (void*)this);
                if (fConnectionRequestHandlerID > B_NO_ERROR)
                {
                    ::resume_thread(fConnectionRequestHandlerID);
                }
                
                // Start separate thread to listen for incoming client data.
                fClientListenerID = ::spawn_thread(ListenToClients, 
                                                kClientListenerName,
                                                fPriority,
                                                (void*)this);
                if (fClientListenerID > B_NO_ERROR)
                {
                    ::resume_thread(fClientListenerID);
                }
            }
        }

That's it for Run().Lets look at HandleConnectionRequests(). It's a static private method of the BTSNetMsgServer class.

long
BTSNetMsgServer::HandleConnectionRequests(void* arg)
{
    BTSNetMsgServer*    thisServer = (BTSNetMsgServer*)arg;
    BTSSocket           acceptSocket = thisServer->Socket();
    int                 clientSocket;
    sockaddr_in         clientInterface;
    int                 clientIntfSize = sizeof(sockaddr_in);
    long                result = 0;
    PRINT( ("HandleConnectionRequests - THREAD ENTER\n"));
    // Thread blocks here, on accept().
    while ((clientSocket = ::accept(acceptSocket.ID(), 
                                    (struct sockaddr*)&clientInterface,
                                    &clientIntfSize)) >= 0)
    {        
        PRINT(( "Connection request, new socket is %d\n", clientSocket));
        // A client has requested a connection. Make a handler for the
        // client socket.

        if (clientSocket >= 0)
        {
            BTSSocket* newClient = new BTSSocket(clientSocket);
            // Tell the server about the new client.
            BMessage* aMessage = new BMessage(NEW_CLIENT_MSG);
            if (aMessage != NULL)
            {
                if (aMessage->Error() == B_NO_ERROR)
                {
                    aMessage->AddObject(SOURCE_SOCKET, (BObject*)newClient);
                    if (aMessage->Error() == B_NO_ERROR)
                    {
                        thisServer->PostMessage(aMessage);
                    }
                    else delete aMessage;
                }
                else delete aMessage;
            }
        }
        else break;
        clientIntfSize = sizeof(sockaddr_in);
    }
    PRINT( ("HandleConnectionRequests - THREAD EXIT\n"));
    exit_thread(result);
}

As noted earlier, this thread is constantly blocked on the accept() function, which only returns when a client has requested a connection. Each time it returns, we get the client's local socket number. (The socket number on the client's machine may be different) When this occurs we create a new BTSSocket object representing the client, place it in a BMessage and post the message to the server. The server maintains a list of socket objects.

Now that the client is connected, the server may receive data from it, which would arrive in the thread running the server's ListenToClients() method. Let's look at the main loop for ListenToClients:

long
BTSNetMsgServer::ListenToClients(void* arg)
{
    BTSNetMsgServer*    thisServer = (BTSNetMsgServer*)arg;
    BList*              serverSocketList = thisServer->SocketList();
    BLooper*            messageReceiver = thisServer->MessageReceiver();
    BMessage*           newMessage = NULL;
    sem_id              socketListSem = thisServer->SocketListSem();
    long                result = B_NO_ERROR;
    struct timeval      tv;
    struct fd_set       readBits;
    BTSSocket*          socket;
    int                 i;
    
    // Set delay to wait for client data before returning.
    tv.tv_sec = 1;
    tv.tv_usec = 0;

    PRINT(("BTSNetMsgServer::ListenToClients - THREAD ENTER\n"));
    
    for (;;)
    {
        // Set the select bits for the known sockets
        FD_ZERO(&readBits);
        PRINT(("Acquiring socket list semaphore\n"));
        ::acquire_sem(socketListSem);
        BList socketList(*serverSocketList);
        ::release_sem(socketListSem);
        
        if (socketList.IsEmpty()) goto LOOPEND;
        for (i = 0; i < socketList.CountItems(); i++)
        {
            socket = (BTSSocket*)socketList.ItemAt(i);
            FD_SET(socket->ID(), &readBits);
        }
        
        // Blocks here until data arrives on any socket.
        if (::select(32, &readBits, NULL, NULL, &tv) <= 0) goto LOOPEND;

The fd_set structure, readBits, acts like a mask that tells which sockets we are interested in looking at. It is first cleared by FD_ZERO. Then, one by one the client sockets are added to the mask. Once all the sockets are added, we pass readBits as a parameter to select(). What select() does is just wait until either data arrives on a socket or it times out (based on the tv struct value passed as the last parameter). In this case the timeout value is one second. Select can also be used to wait until it safe to write to a socket (ie, no other operations are occuring on the socket) and sideband communication. Don't worry about these now, because they're not in DR8! (support for writeBits is already in DR9)

OK, so if select returns a non-negative value, data has arrived. We want to take the data, turn it into a BMessage, and post it to the server's assigned message receiver. But first, we have to figure out which of the sockets in the mask has data waiting. When select() returns, it turns off bits relating to sockets that don't have data waiting to be read, and leaves on bits for those that do. A call to FD_ISSET for each socket is all we need to check for data on every client socket. If it's set, create the BMessage and post it:

            
        PRINT(("Server received data\n"));
        for (i = 0; i < socketList.CountItems(); i++)
        {
            socket = (BTSSocket*)socketList.ItemAt(i);
            
            // Did data arrive on this socket?
            if (!(FD_ISSET(socket->ID(), &readBits))) goto SOCKETEND;
        
            // Yes..assume flattened BMessage format and go get it
            result = ReceiveNetMessage(*socket, &newMessage);
            
            if ( result != B_NO_ERROR ) goto SOCKETEND;
            
            // Post it to the message receiver.
            messageReceiver->PostMessage(newMessage);

ReceiveNetMessage is a utility function that receives the data and converts it to a BMessage. We'll look at that in a minute. OK, so what if something goes wrong? ReceiveNetMessage is designed to return the error ECONNABORTED if it determines that client socket is dead. If this occurs, it posts a message to the server informing it of this problem:

            // Clean up before checking next socket.
            SOCKETEND:
                            
            if (result == ECONNABORTED)
            {
                // Inform the server that a client went away.
                BMessage*    deadMessage = new BMessage(DEAD_CONNECTION_MSG);
                BMessenger* messenger = new BMessenger(thisServer);
                BMessage*     reply = NULL;
                
                PRINT(("Connection aborted\n"));
                if (deadMessage != NULL && messenger != NULL)
                {
                    if (deadMessage->Error() == B_NO_ERROR)
                    {
                        deadMessage->AddObject(SOURCE_SOCKET, (BObject*)socket);
                        if (deadMessage->Error() == B_NO_ERROR)
                        {
                            messenger->SendMessage(deadMessage, &reply);
                        }
                        else delete deadMessage;
                    }
                    else delete deadMessage;
                }
                
                if (messenger != NULL) delete messenger;
                if (reply != NULL) delete reply;
            }
            result = B_NO_ERROR;
        }

Note that the message indicating the client's exit is sent via a messenger, instead of just being posted. This makes the sending a synchronous call, to ensure that the server has removed the socket from the socket list before the ListenToClients() thread scans the socket list again. Using dead sockets when doing network functions is a sure-fire way to mess up your application.

Another note on receiving data this way; if select() is called with a mask containing more that one socket, it spawns threads, so this makes this method a little inefficient. A better implementation would create threads for each socket, all posting data back to the same message receiver and server.

Sending data to clients

To send data to a client, we post is to the server object, where it is handled in the MessageReceived() method. If a message received by the server is not a control message, it is either sent to a specific client or to all clients, based on whether the message contains a TARGET_SOCKET:

        default:
            BTSSocket* socket; 
            if (inMessage->HasObject(TARGET_SOCKET))
            {
                socket = (BTSSocket*)inMessage->FindObject(TARGET_SOCKET);
                SendNetMessage(*socket, inMessage);
            }    
            else if (!fClientSocketList.IsEmpty())
            {
                long result = SendToClients(inMessage);
            }
        break;

We'll look at SendNetMessage() in the next section. SendToClients() just calls SendNetMessage() for each currently-connected client.

That's it for the generic server. The draw server creates an instance of this server, receives draw messages from clients, and just repeats them back out to all other clients. In addition, it accrues a local bitmap of the drawing so that when new clients connect, they get a copy of the current bitmap from the server. If you like, check out BTSDrawServer.cpp now.


Making a Client

Clients also need to create sockets. Unlike servers, the don't bind to a local port, they bind to a remote port where the server is connected. And they don't listen or accept connections, they just connect to a single server. So, the procedure for bringing a client to life is:

  1. Create a socket.
  2. Connect to the server.

Creating a socket is just like with the server; just create a BTSSocket object, which BTSNetMsgClient does in the constructor.

Connecting to the server

Instead of binding to a local address, clients connect to a remove address. BTSSocket implements this in its Connect() method, which calls the network function connect(). connect() is a blocking function, so instead of calling it directly, when the client is run, a separate thread is started to connect the client to the server. If the connect attempt is successful, the client is notified via a BMessage, and the thread exits:

long 
BTSNetMsgClient::ConnectToServer(void* arg)
{
    // Static function that runs in a separate thread. His whole purpose
    // is to connect to a server, then he goes away. This prevents the
    // main client thread from blocking if a server isn't immediately
    // available.
    BTSNetMsgClient*     thisClient = (BTSNetMsgClient*)arg;
    BTSAddress           address = thisClient->Address();
    BTSSocket            socket = thisClient->Socket();
    int                  result;
        
    // Specify server connection info.
    result = socket.ConnectToAddress(address);    
    if (result >= 0 && !(thisClient->IsExiting()))
    {
        // Since we connected ok, create a socket handler for the 
        // client socket. Also, notify client that we are connected.
        thisClient->PostMessage(CONNECT_MSG);
        PRINT(("connected to server!\n"));        
    }
    exit_thread(result);
}

Listening to the server

As with the server, the client runs a separate thread to listen for incoming data. This occurs in BTSNetMsgClient::MessageReceived(), after the client receives a message indicating that the socket connected successfully. This thread runs the ListenToServer() static method:

long 
BTSNetMsgClient::ListenToServer(void* arg)
{
    BTSNetMsgClient*    thisClient = (BTSNetMsgClient*)arg;
    BLooper*            messageReceiver = thisClient->MessageReceiver();
    BMessage*           newMessage = NULL;
    struct fd_set       readBits;
    struct timeval      tv;
    long                result;
    BTSSocket           socket = thisClient->Socket();
    int                 socketID = socket.ID();
    bool                exit = FALSE;
    
    tv.tv_sec = 1;
    tv.tv_usec = 0;
    while (exit == FALSE)
    {
        FD_ZERO(&readBits);
        FD_SET(socketID, &readBits);
        if (::select(socketID + 1, &readBits, NULL, NULL, &tv) > 0)
        {
            if (FD_ISSET(socketID, &readBits))
            {
                PRINT(("BTSNetMsgClient::ListenToServer - SERVER MSG on %d\n", 
                            socketID));
                result = ReceiveNetMessage(socket, &newMessage);
                if (result == B_NO_ERROR && newMessage != NULL)
                {
                    // Post it to the message receiver.
                    messageReceiver->PostMessage(newMessage);
                }
                
                else if (result == ECONNABORTED)
                {
                    // Connection has died
                    if (!thisClient->IsExiting())
                    {
                        thisClient->PostMessage(DEAD_CONNECTION_MSG);
                    }
                    exit = TRUE;
                }
            }

        }
         if (thisClient->IsExiting())
        {
            exit = TRUE;
        }
    }
    exit_thread(result);
}

This should look very similar to the server, except we need only do select with a single socket descriptor.

Other than this, the client works similar to the server; post a message to it and it goes over the net to the server, receive message from the server via it. It uses the same utility and socket classes as the server.

The draw client creates an instance of BTSNetMsgClient and sends draw commands to it that are received from the draw view. Messages from other clients are received and posted to the window for drawing. The draw message itself just contains two BPoints, for the start and endpoints of the line to be drawn. That's it!


Utility functions

The utility functions do the work of getting network data and converting it back into BMessages, and vice versa.

SafeUnflatten

Passing messages over a network is greatly simplified by the presence of BMessage's Flatten() and Unflatten() methods. However, care needs to be taken when unflattening. In DR8.2, attempting to unflatten a bogus buffer will probably crash your application. This is much improved in DR9. For now we give you SafeUnflatten:

BMessage* SafeUnflatten(const char* buf)
{
    // Safely unflattens a buffer back into a BMessage. Basically, just
    // check for proper message data header ('PARC') before unflattening.
    
    BMessage* newMessage = new BMessage();
    
    PRINT(("SafeUnflatten - ENTER\n"));
    if (buf == NULL) return NULL;
    if (newMessage == NULL) return NULL;
    if ((!(strcmp(buf, "PARC")) && newMessage->Error() == B_NO_ERROR))
    {        
        newMessage->Unflatten(buf);
        if (newMessage->Error() != B_NO_ERROR)
        {
            delete newMessage;
            newMessage = NULL;
        }
    }
    else if (newMessage != NULL)
    {
        delete newMessage;
        newMessage = NULL;
    }
    PRINT(("SafeUnflatten - EXIT\n"));
    return newMessage;
}

Valid message buffers should start with "PARC". One other thing. As William said, "those Error functions are not optional". It's pretty easy to not want to use them when working with messaging, because they end up all over the place. But they are necessary!

Receiving the data used in the unflattening is pretty straightforward. What arrives on the socket is, first, the message buffer size, then the message buffer itself.

ReceiveNetMessage

BMessage data is passed over the network by first sending a long identifier,then sending a long value indicating the message buffer size, then the buffer itself. ReceiveNetMessage wraps up these receives with the call to SafeUnflatten:

long ReceiveNetMessage(const BTSSocket& socket, BMessage** outMessage)
{
    BMessage*           newMessage = NULL;  // Holds new message
    char*               buf;                // Message data buffer
    long                msgSize;            // Message size
    long                result;             // Result of socket calls
    long                msgID = -1;
    
    PRINT(("ReceiveNetMessage - ENTER\n"));
    // Get the header identifying a message.
    socket.RecvLock();
    result = socket.Recv((char*)&msgID, sizeof(long));
    if (result == B_NO_ERROR && msgID == MSG_IDENTIFIER)
    {
        // Get the message size.
        result = socket.Recv((char*)&msgSize, sizeof(long));
        
        msgSize = ntohl(msgSize);    // Convert from network to native format.
    
        if (msgSize >= 0 && result == B_NO_ERROR)
        {
            buf = (char*)malloc(msgSize);    
            if (buf != NULL)
            {
                // Get the message data.
                result = socket.Recv(buf, msgSize);
                if (result == B_NO_ERROR) 
                {
                    // Convert data back into a BMessage.
                    newMessage  = SafeUnflatten(buf);
                    
                    if (newMessage != NULL)
                    {
                        // Add an identifier of where it came from.
                        newMessage->AddObject(SOURCE_SOCKET, (BObject*)&socket);
                    }
                    else result = B_ERROR;
                }
                free(buf);
            }
        }
        else if (msgSize > 0) result = B_ERROR;
    }
    socket.RecvUnlock();
    *outMessage = newMessage;
    
    PRINT(("ReceiveNetMessage - EXIT\n"));
    return result;
}

SendNetMessage

Conversely to ReceiveNetMessage, SendNetMessage sends a BMessage through a socket. The BMessage's Flatten() method is used, following by a call to another utility, SendNetMessageData(). Note that it's our responsibility to free the resulting buffer.

long SendNetMessage(BTSSocket* socket, BMessage* inMessage)
{
    // Converts a BMessage into a buffer and sends it over the specified socket.

    char*    buf = NULL;
    long     numBytes;
    long     result = B_NO_ERROR;
    
    PRINT(("SendNetMessage - ENTER\n"));
    inMessage->Flatten(&buf, &numBytes);

    if (numBytes > 0 && buf != NULL)
    { 
        result = SendNetMessageData(socket, numBytes, buf);
    }
    if (buf != NULL)    free(buf);
    
    PRINT(("SendNetMessage - EXIT\n"));
    return result;
}

SendNetMessageData

A generic routine to send net message data, by first sending an id, then the message buffer size, then the buffer itself. Network data can be sent by using BTSSocket::Send() method, which calls the network kit send() function. In DR8, send() always blocks until all the data is sent or the socket is interrupted.

long SendNetMessage(const BTSSocket& socket, BMessage* inMessage)
{
    // Converts a BMessage into a buffer and sends it over the specified socket.

    char*    buf = NULL;
    long     numBytes;
    long     result = B_NO_ERROR;
    
    PRINT(("SendNetMessage - ENTER\n"));
    inMessage->Flatten(&buf, &numBytes);

    if (numBytes > 0 && buf != NULL)
    { 
        result = SendNetMessageData(socket, numBytes, buf);
    }
    if (buf != NULL)    free(buf);
    
    PRINT(("SendNetMessage - EXIT\n"));
    return result;
}

A semaphore is used to lock the socket for sending, so that the message data doesn't get mixed up with some other message if simultaneous sends are occurring. But, be careful when using semaphores with sockets, it can get you in trouble. Here's why: Suppose a client and server send to each other at the same time, and both send buffers that are bigger than the other side's receive buffer. If each socket object has a semaphore that locks *all* socket activity, the semaphore may be held during the send. However, since the buffer sent is bigger than the receiver's receive buffer, then the send will interrupt before finishing. Continued attempts to send the rest of the data will continue to fail until the other side receives some of the data, thus making space for the rest of the data. But, if both sides are holding socket-specific semaphores, receive can't occur on either side, so both sides can become deadlocked. This is why this example uses separate sempaphores for send and receive.


BTSSocket

You don't have to read this section to use the networking classes described above. However, if you want to know more about the built-in networking functions, read on.

Receiving data on a socket

From BTSSocket::Recv():

long
BTSSocket::Recv(const char* buf, const long bufSize) const
{
    // Receives a network data buffer of a certain size. Does not return until
    // the buffer is full or if the socket returns 0 bytes (meaning it was 
    // closed) or returns an error besides EINTR. (EINTR can be generated when a
    // send() occurs on the same socket.
    
    long result = B_NO_ERROR;    // error value of socket calls
    int  receivedBytes = 0;        
    int  numBytes = 0;
    
    PRINT(("SOCKET %d RECEIVE: ENTER \n", fID));

    while (receivedBytes < bufSize && (result == B_NO_ERROR || result == EINTR))
    {
        PRINT(("Receiving %ld bytes on %d\n", bufSize- receivedBytes, GetID()));
        numBytes = ::recv(fID, (char*)(buf+receivedBytes), 
                            bufSize - receivedBytes, 0);
        
        if (numBytes == 0)
        {
            result = ECONNABORTED;
            break;
        }
        else if (numBytes < 0) 
        {
            PRINT(("error when receiving data!\n"));
            result = errno;
        }
        else 
        {
            receivedBytes += numBytes;
            #if DEBUG
            UpdateReceiveCount(numBytes);
            #endif
        }
    }
    PRINT(("SOCKET %d RECEIVE - Received %ld bytes result is %s\n", fID, numBytes,
                strerror(result)));

    if (result == EINTR && receivedBytes == bufSize) result = B_NO_ERROR;
    PRINT(("SOCKET %d RECEIVE: EXIT\n", fID));
    return result;
}

This method shows a couple of things. First, receives can be interrupted, either by an error or by someone else doing something on the same socket. If the socket is interrupted by someone else, recv will return a number of bytes less than the buffer size. You then need to loop to get the rest of the buffer. (errno will also return EINTR when this occurs.) If the number of bytes returned is less than zero and the error is not EINTR, something bad has happened. If bytes returned == 0, the socket connection has closed.

The value of errno is ONLY valid after receiving a -1 from recv(). Don't check it if you get a postive number or 0.

Sending data on a socket

long
BTSSocket::Send(const char* buf, const long bufSize) const
{
    // Sends the data for a BMessage over a socket, preceded by the message's
    // size.
    
    long    result = B_NO_ERROR;
    int     numBytes = -1;
    int     sentBytes = 0;
    PRINT(( "SOCKET SEND - ENTER, %ld bytes on socket %d\n", bufSize, fID));
    if (bufSize > 0 && buf != NULL)
    {
        while (sentBytes < bufSize && result == B_NO_ERROR || result == EINTR)
        {
            PRINT(("SOCKET SEND - Sending data..\n"));
            numBytes = ::send(fID, buf, bufSize, 0);
            if (numBytes < 0) result = errno;
            if (numBytes > 0) sentBytes += numBytes;
            if (sentBytes < numBytes) result = errno;
            #if DEBUG
            else UpdateSendCount(sentBytes);
            #endif
        }
    }    
    PRINT( ("SOCKET SEND - EXIT, sent %ld bytes on %d result is %s\n", sentBytes, 
                fID, strerror(result)));
    return result;
}

Similar to receiving, send needs to make sure that all the data has been sent, and if it hasn't it must loop and send the rest.This usually occurs because a single send tries to put more data in the receiver's buffer than it can hold.


Other Notes

There are a few problems with DR8 networking. Specifically, simultaneous sending and receiving on a socket can cause lockups. Note that calling select() counts as receiving for this bug; in other words, if you are blocked on select and you send on a socket that is in the select, you may see the bug. Usually this occurs in the server when you have many people drawing at once, and causes the server to stop. This is fixed in DR9.

The use of BMessages as the protocol is not terribly efficient in terms of speed or size. For a network-intensive application, you would likely want to implement your own lower-level protocol. But, if you just need some simple networking so that your apps can communicate with each other, this approach is fast and easy, and probably integrates well into what you've already written.

You can use ps to get some information about what's going on on your application. For example, if you have one server and one client running you see this:

DrawServer (team 26)
    201           DrawServer  sem  10       6      25 rAppLooperPort(13421)
    205        w>Draw Server  sem  15       7       5 Draw Server(13519)
    207                       sem  10       0       1 LooperPort(13535)
    208 Server Request Handler  sem 110       0      15 tcp_receive[201][0](13539)
    209      Client Listener  sem 110       0       0 tcp_receive[208][1](13792)
DrawClient (team 27)
    216           DrawClient  sem  10       7      43 rAppLooperPort(13658)
    224                       sem  10       0       0 LooperPort(13760)
    226 Client socket listener  sem  10       0       1 tcp_receive[216][0](13776)
    228    w>Net Draw Client  sem  15       6       5 Net Draw Client(13806)
net_server (team 14)
     72           net_server  sem  10       6      11 LooperPort(9064)
     86             net main  sem  10       4      16 timeout wait(9082)
     93         ether reader  sem  15       2       8 mace read(9181)
     94        socket server  msg  10       0       2
     95         ether thread  sem  10       5       1 etherwait1(9204)
     96        loopip thread  sem  10       0       0 loop wait(9205)
     97       timeout thread  sem  10       0       0 timeout cancel(9081)
    104       sock:4253,4254  sem  10       0       0 tcp_send[103][0](9349)
    109       sock:4326,4328  sem  10       0       0 tcp_send[108][0](9546)
    206       sock:5879,5880  sem  10       0       0 tcp_send[201][0](13537)
    222       sock:5963,5964  sem  10       0       1 tcp_send[216][0](13774)
    225       sock:5965,5966  sem  10       1       1 tcp_send[208][1](13790)

There's two threads blocked on tcp_receive in the server; one is actually blocked on select (209), the other on accept (208), but this is how they show up. The client also has a thread (226) blocked on select. At the end of the semaphore name, there are two numbers in square brackets; the first is the thread id of the thread that created the socket, and the second is the socket descriptor.

In the net server, there's three threads (206, 222, 225) related to the server and client. Each one represents a 'live' (whether active or not) socket.

When two clients are opened, the effect of this on the select (thread spawning) in the server can be seen (threads 209, 284, and 285). So, multi-socket selects can be pretty inefficient.

DrawServer (team 26)
    201           DrawServer  sem  10       6      26 rAppLooperPort(13421)
    205        w>Draw Server  sem  15       7       5 Draw Server(13519)
    207                       sem  10       1       2 LooperPort(13535)
    208 Server Request Handler  sem 110       0      31 tcp_receive[201][0](13539)
    209      Client Listener  sem 110      44      37 select sem(21244)
    284        select thread  sem  10       0       0 tcp_receive[208][1](13792)
    285        select thread  sem  10       0       0 tcp_receive[208][2](21012)
net_server (team 14)
     72           net_server  sem  10       6      11 LooperPort(9064)
     86             net main  sem  10      22      47 timeout wait(9082)
     93         ether reader  sem  15       9      34 mace read(9181)
     94        socket server  msg  10       0       4
     95         ether thread  sem  10      26       4 etherwait1(9204)
     96        loopip thread  sem  10       1       1 loop wait(9205)
     97       timeout thread  sem  10      20      25 timeout cancel(9081)
    104       sock:4253,4254  sem  10       0       0 tcp_send[103][0](9349)
    109       sock:4326,4328  sem  10       0       0 tcp_send[108][0](9546)
    206       sock:5879,5880  sem  10       0       0 tcp_send[201][0](13537)
    222       sock:5963,5964  sem  10      19      34 tcp_send[216][0](13774)
    225       sock:5965,5966  sem  10      34      35 tcp_send[208][1](13790)
    262       sock:8878,8879  sem  10       0       1 tcp_send[256][0](20965)
    265       sock:8880,8881  sem  10       1       1 tcp_send[208][2](21010)
DrawClient (team 27)
    216           DrawClient  sem  10       7      43 rAppLooperPort(13658)
    224                       sem  10       0       0 LooperPort(13760)
    226 Client socket listener  sem  10      25      32 tcp_receive[216][0](13776)
    228    w>Net Draw Client  sem  15       6       5 Net Draw Client(13806)
DrawClient (team 29)
    256           DrawClient  sem  10       7      42 rAppLooperPort(20743)
    264                       sem  10       0       0 LooperPort(20940)
    266 Client socket listener  sem  10       0       1 tcp_receive[256][0](20967)
    268    w>Net Draw Client  sem  15       2       1 Net Draw Client(21026)

Also included in the source is a simple chat client and server, using the same basic client, server, and socket classes.

 

The BeOS News and Events Developers User Groups Support BeStore BeWare