==============================================================================  
C-Scene Issue #05
Tying Standard Input and Output to a Socket Connection
Platform: UNIX
Kyle R. Burton
==============================================================================  

Synopsis

In this article, I attempt to show how you can establish a socket connection, and tie the standard file descriptors to the socket. This allows you to read from standard input, and write to standard output, which is much simpler than using the socket functions to get input and give output. I recently had a project where I needed to offer the services of a legacy command line program across a network. This program only ran under Unix, and it's services needed to be offerd as is to various clients across a tcp/ip network. The proposed solution was to write a concurrent server that accepted incomming connections, and invoked this legacy command line application while providing a conduit between the socket connection and the command line program's input and output. The source to that project can be obtained from: http://www.voicenet.com/~mortis/projects/tcp_server/tcp_server.html Proposed Solution
I would like to point out that this code is extracted from the actual production code, the major difference is that this code has most of the error handeling removed. Step one is to set up the skleton server code to accept socket connections, and fork a child process to handle the communications with the connected client. The following code should be sufficient for accepting multiple socket connections: --------- Listing 1 --------- #include <stdio.h> #include <unistd.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> int main( int, char** ) { int sck, client, addrlen; struct sockaddr_in this_addr, peer_addr; pid_t child_pid; unsigned short port = 4137; /* random port to listen on */ addrlen = sizeof( struct sockaddr_in ); memset( &this_addr, 0, addrlen ); memset( &peer_addr, 0, addrlen ); this_addr.sin_port = htons(port); this_addr.sin_family = AF_INET; this_addr.sin_addr.s_addr = htonl(INADDR_ANY); sck = socket( AF_INET, SOCK_STREAM, IPPROTO_IP); bind( sck, &this_addr, addrlen ); listen( sck, 5 ); while( -1 != (client = accept( sck, &peer_addr, &addrlen ) ) ) { child_pid = fork(); if( child_pid < 0 ) perror("Error forking"); exit(1); /* error */ if( child_pid == 0 ) { exec_comm_handler(sck); } } exit(0); return 0; } --------- Listing 1 --------- There are four steps necessary for setting up a socket and listening for connections. These are socket(2), bind(2), listen(2), and accept(2). The first, socket(2) is used to obtain a socket file descriptor. The arguments passed to our call to socket(2) are: the domain, for which we pass AF_INET - the address family for the internet. the protocol type, for which we pass SOCK_STREAM, this opens a stream based, connection oriented, reliable socket (like tcp) versus a broadcast, connectionless, unreliable type socket (like udp). and the protocol, which in this case is IPPROTO_IP, which requests a socket from the tcp/ip suite of protocols. If we had chosen a different protocol, then we would not be given a tcp/udp socket, but rather a socket type appropriate for the protocol. Once the call to socket(2) has been made, the socket needs to be bound to a specific port. This is accomplished with the bind(2) call. bind(2) takes a pointer to a struct sockaddr_in (for AF_INET connections, struct sockaddr_in is not used for other networking domains). This structure needs to be initialized before we pass it to the bind call. We set the port (sin_port), address family (sin_family), and the local address (sin_addr.s_addr). The htons(3), and htonl(3) calls are used to put the port, and the local address into network byte order. htons(3) stands for host to network short, while htonl(3) stands for host to network long. Due to the different machine [host] byte orders (remember little endian vs big endian?), a standard had to be set for byte ordering while communicating over sockets. Thankfuly you don't need to worry about the specific order, just how to get your data into that order, in a machine portable way of course. Once the sockaddr_in structure has been initialized, bind(2) can be called. The next, and final step is to set the socket into a state where it is listening for incomming connections. The two parameters to this function are the socket file descriptor, and the size of the backlog you wish to use. The backlog is the number of socket connections you want to have queue up while you're busy doing other things (like servicing other sockets). Now that we have reached this point, we can call accept(2). accept(2) will block untill a client connects to us. When a client connects, accept(2) will return. Once the client connection has been accepted, and the process has forked, the child can then go on to perform the duties of redirecting the input/output file descriptors, and executing the desired program. --------- Listing 2 --------- int exec_comm_handler( int sck ) { close(0); /* close standard input */ close(1); /* close standard output */ close(2); /* close standard error */ if( dup(sck) != 0 || dup(sck) != 1 || dup(sck) != 2 ) { perror("error duplicating socket for stdin/stdout/stderr"); exit(1); } printf("this should now go across the socket...\n"); execl( "/bin/sh", "/bin/sh", "-c", "/path/to/redirected_program" ); perror("the execl(3) call failed."); exit(1); } --------- Listing 2 --------- Our 'comm handler' first closes the file descriptors for standard input, standard output, and standard error. It then uses dup(2) to duplicate the socket file handle. dup(2) will duplicate the given file descriptor and return the duplicate as the next available descriptor. Since 0, 1, and 2 are the next available descriptors, they should be the returned duplicates. Now operations on stdin/stdout/stderr [0/1/2] will act upon the socket instead of the original stdin/stdout/stderr. I was unaware that this technique (calling dup(2) on a socket file descriptor) was possible untill seeing code written by Martin Mares. Conclusion
This technique is not limited to sockets, you can use the dup(2) call on any valid file descriptors. This includes files opened with open(2). So it is possible to use this technique to redirect the input and output of a program to/from disk files or device files under Unix. One thing to look out for is buffering on the socket. The C style io routines (stdio.h) seem to suffer from this more often than the C++ style io routines (iostream.h). If there is a way to disable the inherent buffering in the socket libraries, I am not aware of it, and would appriciate hearing about it. One way to get around this is to flush stdout. Even this is a potential problem, I have not really seen it make a significant impact in any of the situations where I've used this technique.

This page is Copyright © 1998 By C Scene. All Rights Reserved