Writing a network client

Introduction

What follows is my experience of writing a client to support a particular protocol, using the netservicesmin class. I have documented this as I ran into many difficulties along the way, and didn't find the available documentation much help. This document is aimed at people who are begginners to C++, embedded programming and networking (like me).

Getting started

First up you need the library, either the netservices or netservicesmin added to your project and you need to #include the files you need. I wrote a TCP based client, as is most common, so I included TCPSocket.h, DNSRequest.h and EthernetNetIf.h. For info, I was writing a client to implement the STOMP protocol, which is a simple text based extension to the AMQP standard.

The hardware is straightforward, and documented on the Ethernet page.

Getting connected

To get your connection started, you need an ethernet interface instance. This is what talks to the ethernet controller and sends raw data down the wires. This bit is easy, and you can just copy the code below to get it up and running, however see my comments later on connection reliability. Note also that I have used a pointer ethif (which would normally be declared in the header file with this instantiation and setup being declared in the constructor to my class or in some initialisation method).

    printf("Setting up ethernet...\r\n");
    ethif = new EthernetNetIf;
    EthernetErr ethErr = ethif->setup();
    if(ethErr) {
        printf("Error %d in ethernet setup.\r\n", ethErr);
        mbed_reset();
    }
    IpAddr thisip = ethif->getIp();
    printf("My ip=%i.%i.%i.%i\r\n",thisip[0],thisip[1],thisip[2],thisip[3]);

I have also printed out the ip address so you can check everything went ok. This all assumes you are connecting to a network with DHCP enabled and working, which was fine for me. You can set ip addresses and such manually, but that's all documented.

A quick note on callbacks

The TCP socket class and the DNS class use the concept of events and callbacks. Don't worry if you don't understand all of that stuff completely as it's easy to use really. In essence all your doing is telling the program to do something when something else happens. It's implemented as a kind of function pointer which is a bit fiddly in C++, which is why the syntax looks complex.

Getting a TCP socket

The TCP socket is how your protocol will communicate with other devices on the network. All you have to do is read and write blocks of text from/to your socket. So, instantiate the socket (I have used a pointer again) and assign your callback function. As I noted above this just tells the socket what function to call when it wants to tell you something. The Host class is just a continer with stuff the socket needs to know about, like the server's ip address and port.

Don't know the ip address? That's what DNS is there for. The DNS class also uses a callback function, but it's simple enough to use. I have included all the code below to illustrate how to set it up. Essentially you create a DNS request and then sit there polling the network interface until it's resolved. Then you try to connect to your server and again just sit there polling until it tells you it's connected. I use a bool flag called connecting which gets set to false by the callback functions to tell my connect function when it's done. Easy.

void MyClass::Connect( std::string hostname, int port ) {

    sock = new TCPSocket;
    sock->setOnEvent(this,&MyCLass::onTCPSocketEvent);
    
    server = new Host;
    server->setName(hostname.c_str());
    server->setPort(port);
    printf("do DNS request for %s\r\n",hostname.c_str());
    myDNSreq = new DNSRequest();
    myDNSreq->setOnReply(this, &MyClass::onDNSReply);
    connecting = true;
    DNSRequestErr DNSErr = myDNSreq->resolve(server);
    if(DNSErr) {
        printf("DNS error %d\r\n",DNSErr);
        mbed_reset();
    }
    printf("DNS resolving...\r\n");
    while (connecting) {
        Net::poll();
        wait_us(100);
    }
    connecting = true;
    TCPSocketErr bindErr = sock->connect(*server);
    if (bindErr) {
        printf("Error %d in TCP setup.\r\n", bindErr);
        mbed_reset();
    }
    else printf("Socket setup OK, connecting...\r\n");
    while (connecting) {
        Net::poll();
        wait_us(100);
    }
    printf("Socket connected...\r\n");
}

void MyClass::onTCPSocketEvent(TCPSocketEvent e) {
    switch(e) {
        case TCPSOCKET_READABLE: // Data ready to read
            printf("CB:R\r\n");
            Read_data();
            break;
        case TCPSOCKET_CONNECTED: // Socket connected
            printf("CB:C\r\n");
            connecting = false;
            break;
        case TCPSOCKET_WRITEABLE: // Socket open for writing
            printf("CB:W\r\n");
            break;
        case TCPSOCKET_CONTIMEOUT: // Connection timeout
            
        case TCPSOCKET_CONRST: // Remote host connection reset
            
        case TCPSOCKET_CONABRT: // Connection aborted
            
        case TCPSOCKET_ERROR: // Unknown error
            printf("Error %d in connection, restarting.\r\n", e);
            Disconnect();
            mbed_reset();
            break;
        case TCPSOCKET_DISCONNECTED: // Disconnected
            printf("Socket disconnected, restarting\r\n");
            Disconnect();
            mbed_reset();
            break;
    }
}

void MyClass::onDNSReply(DNSReply r) {
   if( r != DNS_FOUND ) {
     printf("Could not resolve hostname.\r\n");
     mbed_reset();
   }  
   printf("DNS Resolved to %d.%d.%d.%d.\r\n",server->getIp()[0],server->getIp()[1],server->getIp()[2],server->getIp()[3]);
   myDNSreq->close();
   delete myDNSreq;
   myDNSreq = NULL;
   connecting = false;
}

So what does Net::poll() do?

Well, it polls the net. It looks for data to be read from/written to the ethernet interface, and having done that it looks for anything the TCP socket needs to read/write. You can inherit the Netservices class if you want (as the HTTP client example does) so that your class can have its own virtual poll() function to be called once everything else has, but I didn't find any need for this.

You have to call Net::poll(). If you don't, nothing will happen, so the main running part of your program will probably be an infinite loop which calls Net::poll() continually.

So what do these events do?

I noted above that the TCP socket is passed a pointer to a callback function which is your event handler. In the code above you can see that I sit there waiting for a CONNECTED event to occur before continuing with my program. You don't need to call the event handler, it is called automatically (everytime you call Net::poll() and there is an event).

The two events you will be concerned about during program operation are the READABLE and WRITABLE events (since that's what your client wants to do - hopefully). Don't sit there waiting for a WRITABLE event to write data to the socket, it doesn't work like that. Just write the data, and hey presto you get a WRITABLE event. Strange I know. You can sit there and wait for a READABLE event however. In my code above I call a Read_data() method when this event occurs which reads the incoming data (obviously).

So why doesn't it work?

Who knows, lots of things can go wrong in a system like this. You should be able to get up and running with the code posted however.

One thing I did have problems with was the mbed freezing on the Net::poll() call after about 5 mins of operation. In the end I manually set the ethernet connection to 10Mbps half-duplex rather than 100Mbps full-duplex which fixed my problems. If you want to do this you need to create a pointer to the mbed Ethernet type called eth (Ethernet *eth;) and then add the code below to your network setup, just below the ethif->Setup() bit. This accesses the interface used by the EthernetNetIf class and overwrites its default settings.

 eth->set_link(Ethernet::HalfDuplex10);


Please log in to post comments.