|
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.] IntroductionThe 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,
Both the client and the server are constructed in two layers; one layer
consists of a generic
On top of this is the drawing server and the drawing client code. These operate
by creating instances of the 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 ServerFirst, let's examine the generic message server.
The server is a
The drawing server also creates a
Preparing to start the serverServers 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
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:
The specifics of doing these things are hidden inside of the
The 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
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 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
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
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 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
Now that the client is connected, the server may receive data from it, which would
arrive in the thread running the server's 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
OK, so if
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);
// 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
Another note on receiving data this way; if
Sending data to clients
To send data to a client, we post is to the server object, where it is handled in
the
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
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
Making a ClientClients 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
Connecting to the server
Instead of binding to a local address, clients connect to a remove address.
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 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 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
Utility functions
The utility functions do the work of getting network data and converting it back
into
SafeUnflattenPassing messages over a network is greatly simplified by the presence of
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
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; }
SendNetMessageConversely to 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
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.
BTSSocketYou 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 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. (
The value of
Sending data on a socketlong 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,
Other NotesThere 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
You can use 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 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 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.
|