Difference between revisions of "Receive-only TCP Client"

From Nutwiki
Jump to: navigation, search
m (Once again.)
 
m (1 revision imported)
 
(No difference)

Latest revision as of 17:03, 27 October 2016

Explaining the Code

The following sample will open a TCP connection to a remote server and receive all data sent by the remote.

<source lang="c"> int main(void) {

   uint32_t baud = 115200;
   TCPSOCKET *sock;
   uint32_t rip;
   int got;
   NutRegisterDevice(&DEV_DEBUG, 0, 0);
   freopen(DEV_DEBUG_NAME, "w", stdout);
   _ioctl(_fileno(stdout), UART_SETSPEED, &baud);
   puts("\nTCP Client Test");
   RegisterEthernet();
   ConfigureIp();
   rip = GetRemoteIp();
   for (;;) {
       printf("Creating socket...");
       sock = NutTcpCreateSocket();
       if (sock == NULL) {
           Fatal();
       }
       puts("OK");
       printf("Connecting %s:%u...", inet_ntoa(rip), REMOTE_PORT);
       if (NutTcpConnect(sock, rip, REMOTE_PORT)) {
           Fatal();
       } else {
           puts("OK");
           for (;;) {
               printf("Receiving...");
               got = NutTcpReceive(sock, buff, sizeof(buff));
               if (got <= 0) {
                   puts("broken");
                   break;
               } else {
                   printf("%d bytes\n", got);
               }
           }
           printf("Disconnecting %s...", inet_ntoa(rip));
       }
       NutTcpCloseSocket(sock);
   }
   Fatal();
   return 0;

} </source>

Debug Output

In order to follow the progress of our sample, the DEBUG device is used for stdout. On most platforms this is implemented by an RS-232 interface.

<source lang="c"> NutRegisterDevice(&DEV_DEBUG, 0, 0); freopen(DEV_DEBUG_NAME, "w", stdout); _ioctl(_fileno(stdout), UART_SETSPEED, &baud); puts("\nTCP Client Test"); </source>

Ethernet Interface

Like we did with the DEBUG device, we also need to register the Ethernet interface. This is done by calling

<source lang="c"> RegisterEthernet(); </source>

This function in turn calls the Nut/OS function NutRegisterDevice and checks its result.

<source lang="c"> static void RegisterEthernet(void) {

   printf("Register %s...", DEV_ETHER_NAME);
   if (NutRegisterDevice(&DEV_ETHER, 0, 0)) {
       Fatal();
   }
   puts("OK");

} </source>

IP Configuration

The configuration is a bit more complex, using optional features like DHCP, default routing or DNS query. Again we use a separate function.

<source lang="c"> ConfigureIp(); </source>

The called function is

<source lang="c"> static void ConfigureIp(void) {

   printf("Configure %s...", DEV_ETHER_NAME);
   if (NutDhcpIfConfig(DEV_ETHER_NAME, 0, 60000)) {
       Fatal();
   }
   puts("OK");
  1. if defined(GATEWAY_IP)
   printf("Set default route %s...", GATEWAY_IP);
   if (NutIpRouteAdd(0, 0, inet_addr(GATEWAY_IP), &DEV_ETHER)) {
       Fatal();
   }
   puts("OK");
  1. endif
  1. if defined(PRIMARY_DNS)
   printf("Set primary DNS " PRIMARY_DNS);
   {
       uint32_t prim_dns_ip = inet_addr(PRIMARY_DNS);
  1. if defined(SECONDARY_DNS)
       uint32_t scnd_dns_ip = inet_addr(SECONDARY_DNS);
       printf("and secondary DNS " SECONDARY_DNS "...");
       NutDnsGetConfig2(NULL, NULL, &prim_dns_ip, &scnd_dns_ip);
  1. elif
       printf("...");
       NutDnsGetConfig2(NULL, NULL, &prim_dns_ip, NULL);
  1. endif
   }
   puts("OK");
  1. endif

} </source>

Some optional settings are defined by pre-processor macros, which will be explained later. However, the local IP address is expected in non-volatile memory when calling NutDhcpIfConfig. If there is no initial configuration stored or if the local IP address is 0.0.0.0, then this function will automatically try to query all IP settings from a local DHCP server. This will typically include the IP addresses of a default gateway and one or two DNS servers.

In the next step the main function will call GetRemoteIp to retrieve the IP address of the remote host. In Nut/OS applications IP addresses are represented by 4 byte values.

<source lang="c"> uint32_t rip;

rip = GetRemoteIp(); </source>

This local function checks, whether REMOTE_HOST specifies an IP address or hostname. In the latter case NutDnsGetHostByName is called to retrieve the IP address from the configured DNS server.

<source lang="c"> static uint32_t GetRemoteIp(void) {

   uint32_t rc;
   rc = inet_addr(REMOTE_HOST);
   if (rc == (uint32_t)-1) {
       printf("Resolving %s...", REMOTE_HOST);
       rc = NutDnsGetHostByName((uint8_t *)REMOTE_HOST);
       if (rc == 0) {
           Fatal();
       }
       puts(inet_ntoa(rc));
   }
   return rc;

} </source>

Establishing a Connection

As soon as the IP interface is up and running, a TCP connection to a remote host will be established. First we need to create a TCP socket by calling NutTcpCreateSocket.

<source lang="c"> TCPSOCKET *sock;

sock = NutTcpCreateSocket(); if (sock == NULL) {

   Fatal();

} </source>

If this fails, we are probably out of memory.

Nut/OS requires to create a new socket for each connection. Later when closing the connection, we will call

<source lang="c"> NutTcpCloseSocket(sock); </source>

This will not completely release the socket, because the TCP state machine handles the final handshake in the background. In no case the application should touch the socket structure after this call. For new connections a new socket must be created, calling NutTcpCreateSocket. Of course, the TCPSOCKET pointer may be re-used.

Receiving Data

In this sample application the remote host is expected to send data of any length and then close the connection. Our side will not send anything.

<source lang="c"> for (;;) {

   printf("Receiving...");
   got = NutTcpReceive(sock, buff, sizeof(buff));
   if (got <= 0) {
       puts("broken");
       break;
   } else {
       printf("%d bytes\n", got);
   }

} </source>

A global buffer is used for incoming data.

<source lang="c"> static char buff[1024]; </source>

There are three different results returned by NutTcpReceive.

> 0 The number of bytes received.
0 Timed out while waiting for data.
< 0 Remote closed connection or an error occurred.

Our sample doesn't use any Socket Timeouts. If the link is cut while a connection has been established, the program will be trapped in the receive routine. Newbies are often considering this a bug. In fact it is a feature, allowing TCP to continue with existing connections after a broken link has been fixed.

When the remote gracefully closes the connection while we are listening for more data, NutTcpReceive will return -1, which is identified as a broken connection. Note, that the receive loop is embedded in an endless outer loop, which will try to re-establish the connection in order to receive more data.

Error Handling

For testing purposes, the program will simply stop on any error by calling Fatal(). Normal applications will usually try to recover from failures.

<source lang="c"> static void Fatal(void) {

   puts("Failed, system halted");
   for (;;);

} </source>

Hard-coded Options

Several settings are hard-coded by pre-processor macros at the top of the source code.

<source lang="c">

  1. define REMOTE_HOST "192.168.0.11"
  2. define REMOTE_PORT 2001

//#define GATEWAY_IP "192.168.0.1" //#define PRIMARY_DNS "192.168.0.1" //#define SECONDARY_DNS "192.168.0.2" </source>

Note, that optional settings may be commented out if not used.

REMOTE_HOST IP address or hostname of the remote server to connect.

If a host name is used, make sure, that a DNS server is configured, of which the IP address is either defined by the macro PRIMARY_DNS or provided by DHCP.

REMOTE_PORT TCP port of the remote host to connect.

This must match the port, at which a TCP server is listening at the remote site.

GATEWAY_IP IP address of our default gateway.

We do not need this, if the remote host is in the same IP network as we are or if DHCP is used for IP configuration.

PRIMARY_DNS IP addresses of the primary DNS servers.

This is required only, if REMOTE_HOST specifies a host name (instead of an IP address) and if DHCP is not available or doesn't provide this information.

SECONDARY_DNS IP addresses of the secondary DNS servers.

If this is defined, then PRIMARY_DNS must be defined as well.

Included Header Files

#include <stdio.h>

Provides function prototypes for stdio calls like printf, puts etc.


#include <io.h>

Contains low level function prototypes like ioctl.


#include <dev/board.h>

Provides board-specific settings like DEV_DEBUG or DEV_ETHER_NAME.


#include <sys/socket.h>

Prototypes provided by the socket API.


#include <arpa/inet.h>

Various IP utility functions and macros.


#include <net/route.h>

Function prototypes used for IP routing.


#include <netdb.h>

Required for DNS usage.


#include <pro/dhcp.h>

Contains the prototype for NutDhcpIfConfig, which is used to configure local IP settings.

Remote Server

For running the test, we created a server program that is able to run on a PC, either under Linux or Windows.

<source lang="c">

  1. include <stdio.h>
  2. include <string.h>
  1. ifdef _WIN32

typedef int socklen_t;

  1. include <winsock.h>
  2. define WINSOCK_VERSION (1 | (1 << 8))
  1. else /* WIN32 */
  1. include <netdb.h>
  2. include <sys/socket.h>
  3. include <arpa/inet.h>

typedef int SOCKET;

  1. define INVALID_SOCKET -1
  2. define SOCKET_ERROR -1
  3. define closesocket(s) close(s);
  1. endif /* WIN32 */

static char buf[1024];

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

   int sock;
   int csock;
   unsigned short port;
   struct sockaddr_in server;
   struct sockaddr_in client;
   int namelen;
   int got;
  1. if defined WIN32
   WSADATA wsa_data;
   WSAStartup(WINSOCK_VERSION, &wsa_data);
  1. endif
   if(argc != 2) {
       fprintf(stderr, "usage: tcpserver <port>\n");
       port = 2001;
       //return 1;
   } else {
       port = atoi(argv[1]);
   }
   if((sock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
       fprintf(stderr, "Opening tcp socket failed\n");
       return 3;
   }
   memset(&server, 0, sizeof(server));
   server.sin_family = AF_INET;
   server.sin_port = htons(port);
   server.sin_addr.s_addr = INADDR_ANY;
   if(bind(sock, (void *)&server, sizeof(server)) < 0) {
       fprintf(stderr, "Binding server socket failed\n");
       closesocket(sock);
       return 3;
   }
   fprintf(stderr, "Port %u\n", port);
   if(listen(sock, 5) != 0) {
       fprintf(stderr, "Listening failed\n");
       closesocket(sock);
       return 3;
   }
   for(;;) {
       namelen = sizeof(client);
       if((csock = accept(sock, (void *)&client, &namelen)) == -1) {
           fprintf(stderr, "Accepting connection failed\n");
           closesocket(sock);
           return 3;
       }
       fprintf(stderr, "Connected\n");
       send(csock, "Hello", 5, 0);
       closesocket(csock);
       fprintf(stderr, "\nDisconnected\n");
   }
   return 0;

} </source>

Problems

NutTcpConnect Returns Wrong Error

Beginning of 2010 Malte reported a problem in the Ethernut mailing list.

Indeed, if the server sends a few bytes of data only and immediately closes the connection, then NutTcpCreateSocket returns with an error, although the data had been successfully transmitted.

Malte detected the cause of this problem in the TCP state machine, specifically in the function NutTcpStateActiveOpenEvent.

<source lang="c"> int NutTcpStateActiveOpenEvent(TCPSOCKET * sock) {

   NutTcpStateChange(sock, TCPS_SYN_SENT);
   if(sock->so_state == TCPS_SYN_SENT)
       NutEventWait(&sock->so_ac_tq, 0);
   if (sock->so_state != TCPS_ESTABLISHED)
       return -1;
   return 0;

} </source>

When the application calls NutTcpConnect, Nut/OS will internally call this function. It will move the state of the TCP state machine from TCPS_CLOSED to TCPS_SYN_SENT by sending a SYN segment to the remote. Then it will wait for a connection event in the queue so_ac_tq.

The thread of the state machine will handle the initial TCP handshake (SYN, SYN-ACK, ACK) in the background. As soon as it is completed, an event will be sent to the queue to wake up the application thread. However, the state machine is running at a high priority and may continue its conversation with the remote before the application thread is activated. Here the trouble begins. The remote will send some data, which is stored and acknowledged by the state machine thread. Then the remote closes the connection, moving the connection state from TCPS_ESTABLISHED to TCPS_CLOSE_WAIT. When the application thread finally wakes up, it will no longer find the connection in TCPS_ESTABLISHED state and return an error.

Malte proposed the following update, which fixes the problem.

<source lang="c"> int NutTcpStateActiveOpenEvent(TCPSOCKET * sock) {

   NutTcpStateChange(sock, TCPS_SYN_SENT);
   if(sock->so_state == TCPS_SYN_SENT)
       NutEventWait(&sock->so_ac_tq, 0);
   if (sock->so_state != TCPS_ESTABLISHED && sock->so_state != TCPS_CLOSE_WAIT)
       return -1;
   return 0;

} </source>

Note: Although reported and confirmed at the end of January 2010, no fix appeared in the code repository at the time of this writing, October 2010.

NutTcpConnect Never Returns

Ole and others reported, that under certain conditions NutTcpConnect may never return, freezing the calling application thread.

More on this later.