mTCP Programming Sample

Introduction

mTCP is a fairly large library with lots of features and getting started with it can be daunting. This page should help you by introducing some programming concepts that you will use when writing mTCP applications.

The code I will be discussing is a simplified version of the Netcat program. This program can listen for an incoming socket or make a socket connection to a server. Anything you receive from the socket gets put on the screen and anything you type will be sent to the other end. Alt-H shows onscreen help and Alt-X exits the program.

A zip file with the full version of the code I am using as an example can be found at http://www.brutman.com/mTCP/mTCP_Programming_Sample.zip .

Program startup and reading the mTCP configuration file

All mTCP programs need to read the mTCP configuration file to find out which interrupt the packet driver is using, what the IP address and netmask are, and some other optional settings. A function called Utils::parseEnv does all of this for you:

int main( int argc, char *argv[] ) {

fprintf( stderr, "mTCP Sample program by M Brutman (mbbrutman@gmail.com) (C)opyright 2011\n\n" );

// Read command line arguments
parseArgs( argc, argv );

// Setup mTCP environment
if ( Utils::parseEnv( ) != 0 ) {
exit(-1);
}
The program first checks its own command line parameters by calling its own parseArgs routine. After that it calls Utils::parseEnv so that mTCP can find and read its configuration file. There are no parameters to use but there is a return code to check. Any return code other than 0 is a failure.

Starting the TCP/IP Stack

If you made it this far then mTCP has read its configuration parameters and you are ready to tell it to start sending and receiving packets. The next call makes the stack go live:
  // Initialize TCP/IP stack

if ( Utils::initStack( 2, TCP_SOCKET_RING_SIZE ) ) {
fprintf( stderr, "\nFailed to initialize TCP/IP - exiting\n" );
exit(-1);
}

// From this point forward you have to call the shutdown( ) routine to
// exit because we have the timer interrupt hooked.

// Save off the oldCtrlBreakHander and put our own in. Shutdown( ) will
// restore the original handler for us.

oldCtrlBreakHandler = getvect( 0x1b );
setvect( 0x1b, ctrlBreakHandler);

// Get the Ctrl-C interrupt too, but do nothing. We actually want Ctrl-C
// to be a legal character to send when in interactive mode.

setvect( 0x23, ctrlCHandler);

The first parameter is a number of TCP sockets to create. The second parameter is a number of outgoing TCP buffers to create. Sockets and buffers take up memory space so allocate only what you need. If the function returns anything but zero you have an error and the program should terminate. The most common error is not being able to find the packet driver because it was not loaded or it was loaded at a different interrupt number.

If you succeed then mTCP is alive and possibly receiving and processing packets from the packet driver. On a busy network you will be getting traffic almost immediately. Next it will be time to get into your main program loop to start processing those packets.

mTCP hooks the timer interrupt to be able to keep track of time more efficiently than the C runtime can. If your program runs far enough to initialize the TCP/IP stack (past Utils::initStack) then you need to ensure that your program will call the routine to end mTCP gracefully (Utils::endStack).

Unless your program crashes in a horrible manner this should not be a problem. But one major hole that people forget about is Ctrl-Break; somebody pressing this can force your program to end early without properly shutting down mTCP. To prevent this from happening you will see some code that hooks the Ctrl-Break and Ctrl-C interrupt vectors. The new Ctrl-Break handler sets a flag that we can check in our main program loop.

Getting a socket connection


The next part of the program involves either waiting for an incoming socket connection or making a socket connection to another system. The choice is made on the command line. When waiting for a socket from another system your program behaves like a server. When contacting another system your program behaves like a client.

Resolving a server name and connecting to a server

If you are making a connection to another system you will need to go through DNS name resolution and do a socket connect:
  TcpSocket *mySocket;

int8_t rc;
if ( Listening == 0 ) {

fprintf( stderr, "Resolving server address - press Ctrl-Break to abort\n\n" );

IpAddr_t serverAddr;

// Resolve the name and definitely send the request
int8_t rc2 = Dns::resolve( ServerAddrName, serverAddr, 1 );
if ( rc2 < 0 ) {
fprintf( stderr, "Error resolving server\n" );
shutdown( -1 );
}
Dns::resolve is used to convert server names to IP addresses. If you used a numerical IP address then the first call to Dns::resolve will resolve it and you will not have to wait. Otherwise, you need to enter a loop to wait for DNS to complete.
    uint8_t done = 0;

while ( !done ) {

if ( CtrlBreakDetected ) break;
if ( !Dns::isQueryPending( ) ) break;

PACKET_PROCESS_SINGLE;
Arp::driveArp( );
Tcp::drivePackets( );
Dns::drivePendingQuery( );

}
The loop has to process incoming packets, retry Arp requests if necessary, and retry DNS requests if necessary. Those functions handled by PACKET_PROCESS_SINGLE, Arp::driveArp, and Dns::drivePendingQuery.

(This DNS implementation is UDP based. Dns::drivePendingQuery is only needed because UDP packets can get lost, and we need a way to detect if we need to resend our DNS request.)
     // Query is no longer pending or we bailed out of the loop.

rc2 = Dns::resolve( ServerAddrName, serverAddr, 0 );

if ( rc2 != 0 ) {
fprintf( stderr, "Error resolving server\n" );
shutdown( -1 );
}
When DNS completes or times out the loop will end. At the end of the loop another call to Dns::resolve tells you the final result.
    mySocket = TcpSocketMgr::getSocket( );

mySocket->setRecvBuffer( RECV_BUFFER_SIZE );

fprintf( stderr, "Server resolved to %d.%d.%d.%d - connecting\n\n",
serverAddr[0], serverAddr[1], serverAddr[2], serverAddr[3] );

// Non-blocking connect. Wait 10 seconds before giving up.

rc = mySocket->connect( LclPort, serverAddr, ServerPort, 10000 );

}
If DNS resolved the name and gave you an IP address you will need to allocate a socket, set the TCP receive buffer size for the socket, and make a TCP socket connect call. mTCP owns all of the socket data structures - you just get a pointer to them. The call to TcpSocketMgr?::getSocket gets you a socket to use. When you are done with a socket you should return it using the TcpSocketMgr?::freeSocket call.

Listening for an incoming socket

If you decided to listen for an incoming socket instead things are a little different:
  else {

fprintf( stderr, "Waiting for a connection on port %u. Press [ESC] to abort.\n\n", LclPort );

TcpSocket *listeningSocket = TcpSocketMgr::getSocket( );
listeningSocket->listen( LclPort, RECV_BUFFER_SIZE );
Here we allocate a socket using TcpSocketMgr?::getSocket but instead of using this socket to make an outgoing connection to a server we are going to use it to listen for incoming sockets. The listen call tells mTCP what port to listen on and what receive buffer size to use for newly created sockets.
    // Listen is non-blocking.  Need to wait
while ( 1 ) {

if ( CtrlBreakDetected ) {
rc = -1;
break;
}

PACKET_PROCESS_SINGLE;
Arp::driveArp( );
Tcp::drivePackets( );

mySocket = TcpSocketMgr::accept( );
if ( mySocket != NULL ) {
listeningSocket->close( );
TcpSocketMgr::freeSocket( listeningSocket );
rc = 0;
break;
}

if ( _bios_keybrd(1) != 0 ) {
char c = _bios_keybrd(0);
if ( (c == 27) || (c == 3) ) {
rc = -1;
break;
}
}
}
}
After that, we wait! The loop checks for keyboard input to see if the user wants to end the program early. It also processes incoming packets. (If a user hits Ctrl-Break the CtrlBreakDetected? variable will be set by the new interrupt handler we installed after TCP/IP went live. Instead of ending the program prematurely, we can do an orderly shutdown.)

When an incoming socket connection is received the TcpSocketMgr?::accept call will return a pointer to the socket. At that point we stop listening for sockets by closing the listening socket and we fall into the common code for sending and receiving data on the socket.

Main processing loop

The code is too large to reproduce here verbatim, but here is the general idea:
while ( programIsNotEnding ) {
Process Incoming Packets
Perform some processing on those packets
Check for User input
}
Lets get into more detail ...

Process Incoming Packets

mTCP needs to be told when it can run to process packets. Unlike a modern operating system, it does not happen in the background automatically.

The way to tell mTCP to process packets is to make a series of calls:
    // Service the connection
PACKET_PROCESS_SINGLE;
Arp::driveArp( );
Tcp::drivePackets( );
PACKET_PROCESS_SINGLE is a macro that checks for new packets from the packet driver and if any are found it processes those packets.

Arp::driveArp is used to check up on pending Arp requests and retry them if necessary.

Tcp::drivePackets is used to send any packets that you might have queued up to send.

(Earlier you saw another call: Dns::drivePendingQuery. That functions like these calls, but it is only needed when performing a DNS lookup.)

You could have one function or macro that hides these three functions under the covers. I have them broken up into three different calls so that you can more carefully control when you check for incoming packets vs. when you decide to push packets out. But in general you will find the code making all three calls at the same time.

Perform some processing on those packets

This program is a simplified version of the Netcat program. So its major job is to receive and display things from the socket and to send user keystrokes out on the socket.

This is the code that checks the socket for new data:
    if ( mySocket->isRemoteClosed( ) ) {
done = 1;
}

// Process incoming packets first.

int16_t recvRc = mySocket->recv( recvBuffer, RECV_BUFFER_SIZE );

if ( recvRc > 0 ) {
write( 1, recvBuffer, recvRc );
}
else if ( recvRc < 0 ) {
fprintf( stderr, "\nError reading from socket\n" );
done = 1;
}
If the socket is not closed then the code will try to receive data on it. A return code of 0 indicates no data, which is not an error. A negative return code indicates a socket error and a positive return code is the number of bytes that were received. Any bytes that are received are written to to STDOUT right away.

Here is the code that checks for user input:
    if ( CtrlBreakDetected ) {
fprintf( stderr, "\nCtrl-Break detected\n" );
done = 1;
}

if ( _bios_keybrd(1) ) {

uint16_t key = _bios_keybrd(0);
char ch = key & 0xff;

if ( ch == 0 ) {

uint8_t ekey = key >> 8;

if ( ekey == 45 ) { // Alt-X
done = 1;
}
else if ( ekey == 35 ) { // Alt-H
fprintf( stderr, "\nSample: Press Alt-X to exit\n\n" );
}

}
else {
int8_t sendRc = mySocket->send( (uint8_t *)&ch, 1 );
// Should check the return code, but we'll leave that
// as an exercise to the interested student.
}
}
Note the first check is to check that the user has not hit Ctrl-Break. If they did our interrupt handler would have set the flag, which tells us to end the program.

If the user hits a key we read the keyboard and process the key as appropriate. We recognize special keys Alt-H (Help) and Alt-X (Exit). Any other keystroke gets sent on the socket using the send call.

The actual packet does not get sent on the wire until you make the Tcp::drivePackets( ) call. That code also handles resending the packet if it gets lost along the way.

Ending your program gracefully

Once the TCP/IP stack is initialized and receiving packets you need to ensure that it gets shut down properly before your program ends. If you don't your system will eventually crash because of the dangling timer interrupt.

Calling Utils::endStack will do the trick. In my code I usually have a shutdown routine that I can call from anywhere that will make the Utils::endStack call and return to DOS:
static void shutdown( int rc ) {

setvect( 0x1b, oldCtrlBreakHandler);

Utils::endStack( );
Utils::dumpStats( stderr );
fclose( TrcStream );
exit( rc );
}
Utils::endStack will take care of the timer interrupt that it had hooked. Since my code installed the interrupt handler for Ctrl-Break it is responsible for removing it. Both the Ctrl-Break and Ctrl-C handlers were installed but only the Ctrl-Break handler needs to be restored. DOS will take care of restoring the Ctrl-C handler automatically.

Building the sample

To build the sample program you should have Open Watcom installed. You should be able to compile a test program with it.

The easiest way to build the sample is to:
The included Makefile is already setup to find all of the mTCP include files. Wmake will use the Makefile and create an executable for you. The executable can then be run on your favorite real or emulated DOS machine.

Summary

The sample program showed you the following techniques:

The real Netcat program isn't that much more complicated - it just handles all of the special processing needed for Telnet newline processing, screen echoing, and it does some tricks for better performance when sending and receiving data from files. You will find this makes a useful skeleton program for your own network applications.


Originally created in 2011, Last updated July 9th, 2016
(C)opyright Michael B. Brutman, mbbrutman at gmail.com