Netbounce Example

by: burt rosenberg
at: february 2017


Our first trip on the information highway!

Netbounce

To get started on internet programing, let's try to bounce a packet of a distant machine. True, this is not as hopelessly techno-romantic as moon bouncing, but I would think there would be a thrill of accomplishment as you take your first trip on the information highway.

The following pair of programs are a server-client pair. The server program runs on one machine listening for a request packet from the client program and responds back to the client with response packet. The client program runs on another machine (although it could run on the same machine), sending a request packet to the server and listening for the response packet. This pattern of request-reply initiated by the client and responded to by the server is the basis for client-server computer network interactions.

Your gateway to the information highway is the the socket, more properly the Berkeley Socket. A socket is a software abstraction that summarizes all of the networking machinery into a file handle. The socket handle is very similar to a standard file handle in that it supports the actions of reading and writing. But the API supports a few special actions, e.g. to set the destination address, or to sense the source address.

Note on bound and connected UDP sockets

UDP sockets can be bound or unbound, and connected or unconnected.

The bind call assigns a port number to the socket and from that moment packets to the port from any host are put on the socket's receive queue. Recvfrom must be called on a bound socket, and sendto, if called on an unbound socket will bind it to an default interface at an ephemeral port. A UDP socket sends and receives on the same port. Once bound, both the destination and the source ports will be the bound port number.

If the socket becomes connected, it is associated with a specific source host and port and the queue will be restricted to packets from that port on that host. Connecting only occurs if a connect call occurs to the socket.

Connected UDP sockets are curious things. Only a connected socket can receive ICMP packets (see this faq). Once connected using the connect call, the usage of sendto and recvfrom is restricted — at best the address structure will be ignored. There seems to be a critical race between arriving packets during the interval between bind and connect which I have not found discussed in the documentation.

We will not use connected UDP sockets. The establishment of a session, which is the effect of socket connection, will be handled by our own software and protocols.



The Server

/* ** netbounce-server.c ** from Beej's Guide to Network Programming: Using Unix Sockets, ** by Brian "Beej" Hall. ** ** modified by Burt Rosenberg, ** Created: Feb 8, 2009 ** Last modified: Jan 31, 2015 - added comment about mild ** bug if received data is binary -bjr ** Jan 12, 2016 - add getopt */ #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<errno.h> #include<string.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<assert.h> #define MAXBUFLEN 100 #define USAGE_MSG "usage: %s [-lv] -p port\n" #define PROG_NAME "netbounce-server" int main(int argc, char * argv[]) { int sockfd; struct sockaddr_in my_addr; struct sockaddr_in their_addr; unsigned int addr_len, numbytes; char buf[MAXBUFLEN]; int ch ; int is_verbose = 0 ; int port = 0 ; int is_loop = 0 ; while ((ch = getopt(argc, argv, "vp:l")) != -1) { switch(ch) { case 'p': port = atoi(optarg) ; break ; case 'v': is_verbose = 1 ; break ; case 'l': is_loop = 1 ; break ; default: printf(USAGE_MSG, PROG_NAME) ; return 0 ; } } argc -= optind; argv += optind; if ( !port ) { printf(USAGE_MSG, PROG_NAME) ; return 0 ; } assert(port) ; if ((sockfd=socket(AF_INET,SOCK_DGRAM,0))==-1) { perror("socket") ; exit(1) ; } my_addr.sin_family = AF_INET ; my_addr.sin_port = htons((short)port) ; my_addr.sin_addr.s_addr = INADDR_ANY ; memset(&(my_addr.sin_zero),'\0',8) ; if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1 ) { perror("bind") ; exit(1) ; } while (1==1 ) { /* while forever */ addr_len = sizeof(struct sockaddr) ; if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN-1,0, (struct sockaddr *)&their_addr, &addr_len)) == -1 ) { perror("recvfrom") ; exit(1) ; } // assume can be a string, and terminate buf[numbytes] = '\0' ; if ( is_verbose ) { /* added to get source port number */ printf("got packet from %s, port %d\n", inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port)) ; printf("packet is %d bytes long\n", numbytes ) ; /* Mild bug: if the incoming data was binary, the "string" might be less than numbytes long */ printf("packet contains \"%s\"\n", buf ) ; } { /* return the packet to sender */ int bytes_sent ; if ((bytes_sent = sendto( sockfd, buf, strlen(buf), 0, (struct sockaddr *)&their_addr, sizeof(struct sockaddr)))==-1 ) { perror("send") ; exit(1) ; } if ( is_verbose ) { printf("packet sent, %d bytes\n", bytes_sent) ; } } if ( !is_loop ) break ; } close(sockfd) ; return 0 ; }

Server code

  1. Sockets, server or client, are created with a call to socket (note the includes: sys/types.h, sys/socket.h, netinet/in.h and apra/inet.h tend to be standard requirements).
  2. Since we are a server we need to bind to our well known port, using a call to bind. This requires that a struct sockaddr_in structure be filled out.
    * Note that the structure is filled out with INADDR_ANY as the source address. This mean the socket will listen on all interfaces. A single machine might have several interfaces, and you can specify a single interface, if that is what is needed.
    * Note the use of the "host-to-network-short" macro htons. The Internet is big-endian. The computer on which the code runs might be big-endian or little-endian. This macro is defined to make the correction (or do nothing) for short data-types.
    * Note the use of sizeof. The structure is variable sized, depending on the protocol, so the called function needs to be told how many bytes are in the passed structure.
  3. The server then listens on the port by a call to recvfrom. This is a blocking call. It will return with the message copied into the buffer, and the number of bytes copied as a return value.
    * Note that there is no way to know how many bytes of the data has been returned is just by looking at the data. Many students are confused between byte data and string data. String data is byte data with a stop signal at the end of the data — a null byte. No such thing for byte data.
    * Note the cast in the call to a struct sockaddr *. This cast takes the subclass sockaddr_in and widens it to a generic sockaddr for the purposes of matching the signature of the called function.
  4. The packet is sent back using a call to sendto. Note that the destination address is passed into the function using the struct sockaddr_in that was filled out by recvfrom. All you need do is pass this struct to the sender and it is properly filled out with return trip data, including the ephemeral port of the client.


The Client

/* ** netbounce-client.c ** from Beej's Guide to Network Programming: Using Unix Sockets, ** by Brian "Beej" Hall. ** ** modified by Burt Rosenberg, ** Created: Feb 8, 2009 ** Last modified: Jan 31, 2015 - minor changes -bjr ** Jan 12, 2015 - added getopt */ #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<errno.h> #include<string.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<netdb.h> #include<assert.h> #define MAXBUFLEN 100 #define USAGE_MESSAGE "usage: %s [-v] -p port -h host message\n" #define PROG_NAME "netbounce-client" int main( int argc, char * argv[] ) { int sockfd ; struct sockaddr_in their_addr ; struct hostent *he ; int numbytes ; int port = 0 ; char * host = NULL ; char * msg = NULL ; int ch ; int is_verbose = 0 ; while ((ch = getopt(argc, argv, "vp:h:")) != -1) { switch(ch) { case 'p': port = atoi(optarg) ; break ; case 'h': host = strdup(optarg) ; break ; case 'v': is_verbose = 1 ; break ; default: printf(USAGE_MESSAGE, PROG_NAME) ; return 0 ; } } argc -= optind; argv += optind; if ( !host || !port || ! argc ) { printf(USAGE_MESSAGE, PROG_NAME) ; return 0 ; } msg = strdup(*argv) ; /* example of an assertion */ assert(host) ; assert(port) ; if ((he=gethostbyname(host))==NULL) { perror("gethostbyname") ; exit(1) ; } if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1 ) { perror("socket") ; exit(1) ; } their_addr.sin_family = AF_INET ; their_addr.sin_port = htons((short)port) ; their_addr.sin_addr = *((struct in_addr *)he->h_addr) ; memset(&(their_addr.sin_zero), '\0', 8 ) ; if ((numbytes=sendto(sockfd, msg, strlen(msg),0, (struct sockaddr *)&their_addr, sizeof(struct sockaddr)) ) == -1 ) { perror("sendto") ; exit(1) ; } if (is_verbose) { printf("send %d bytes to %s\n", numbytes, inet_ntoa(their_addr.sin_addr)) ; } { // if you must introduce new variables, make a block struct sockaddr_in my_addr ; unsigned int addr_len ; char buf[MAXBUFLEN]; addr_len = sizeof(struct sockaddr_in) ; getsockname( sockfd, (struct sockaddr *)&my_addr, &addr_len ) ; if ( is_verbose ) { printf("sent from port %d\n", ntohs(my_addr.sin_port)) ; printf("calling for return packet\n") ; } addr_len = sizeof(struct sockaddr_in) ; if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN-1,0, (struct sockaddr *)&their_addr, &addr_len)) == -1 ) { perror("recvfrom") ; exit(1) ; } if ( is_verbose ) { printf("got packet from %s, port %d\n", inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port)) ; printf("packet is %d bytes long\n", numbytes ) ; } buf[numbytes] = '\0' ; printf("%s\n", buf ) ; } close(sockfd) ; return 0 ; }

Client code

  1. The level 3 endpoint is an IP address. However, what most people have for the endpont is a name. The DNS system is a world wide network of computers that form a dynamic and distributed database to resolve names to IP addresses. For the client to invoke this database, it calles gethostbyname given the (ASCII) name, and returns a struct hostent structure.
    * Note that the memory used by this structure is "property of" the callee. You did not send a pointer, your received a pointer. You have been warned!
  2. A socket is created with a call to socket, just as for the server.
  3. The data is sent with a call to sendto.
    * Note that the client socket is bound with this call to a freshly chosen port number.
  4. To get the source port number, the program makes a call to getsockname.
    * Note that in this case the call fills out a caller allocated struct sockaddr_in, and that it is down-cast to a sockaddr for the purposes of the call.
  5. The bounced packet is received by the client using a call to recvfrom, which also fills out struct sockaddr_in according the the packet information of the returned packet.
    * Note that the small wait I added to the code demonstrates that the receive queue is active immediately after bind, and recvfrom will take a packet from this queue, if the queue is not empty, or block waiting for a packet, if the queue is empty.



The Makefile

# # makefile for netbounce example # Jan 12,2016 # bjr # CC = gcc LOCALHOST = localhost PORT = 3333 all: make netbounce netbounce: netbounce-server netbounce-client netbounce-server: netbounce-server.c ${CC} -o $@ $< netbounce-client: netbounce-client.c ${CC} -o $@ $< test: @echo In one window, make run-server @echo Then, in another window, make run-client run-server: netbounce-server @echo Ready to bounce! ./netbounce-server -lv -p ${PORT} run-client: netbounce-client ./netbounce-client -h ${LOCALHOST} -p ${PORT} -v "The amazing net bounce!" submit: svn commit -m "submitted for grade" clean: -rm netbounce-client netbounce-server

Makefile

  1. No additional libraries are needed — the socket library is included by default.
  2. Testing is done locally, using localhost, a pseudo-target for the local communication that travels the networking stack down to level 2 where it is "looped-back".
Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.

Author: Burton Rosenberg
Created: January 2013
Last Update: January 31, 2020