[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/net_tutorial.tgz.]
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.
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, BMessage
s 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 BMessage
s 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.
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:
BLooper
. This is the one we
post to when we want to send a message to a client or clients.
The drawing server also creates a BApplication
and BWindow
thread, as
usual.
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:
socket()
.
bind()
.
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.
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.
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:
Creating a socket is just like with the server; just create a BTSSocket
object,
which BTSNetMsgClient
does in the constructor.
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); }
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 BPoint
s,
for the start and endpoints of the line to be drawn. That's it!
The utility functions do the work of getting network data and converting it back
into BMessage
s, and vice versa.
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.
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; }
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; }
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.
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.
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.
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.
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 BMessage
s 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 select
s
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.