Simple network applications using sockets (BSD and WinSock) Revision 1 Copyright 2002 - Clifford Slocombe sockets@slocombe.clara.net COPYRIGHT 2002 - CLIFFORD SLOCOMBE PAGE 1 OF 8
Table of Contents Introduction...3 Socket APIs...3 Byte order and structure packing...4 Establishing a communication session...4 Client...4 Server...4 A simple example application...5 Client code for BSD/WinSock...5 Server code for BSD/WinSock...7 Practical applications...8 COPYRIGHT 2002 - CLIFFORD SLOCOMBE PAGE 2 OF 8
Introduction While general-purpose network applications, such as telnet and FTP are useful, it is often necessary to implement customised communicating applications. The ability to leverage the security, flexibility, and universality of TCP/IP is extremely valuable. The ability of TCP/IP to support multiple simultaneous communicating applications makes it well suited to multi-threaded applications, and supports the efficient use of available physical communication links. This document describes only the use of TCP sockets, which provide character stream communication. UDP sockets, which provide data packet, or datagram communication, are not discussed, although much of the information contained herein is applicable to both UDP and TCP. The socket API does not specify any particular communications protocol, however it is most commonly used with TCP/IP. TCP/IP is assumed throughout this document. A WinSock TCP/IP application may communicate with a BSD socket TCP/IP application, allowing cross-platform networking. This document is an introduction to building communicating applications using both BSD and Windows socket APIs. It presents a simple client/server application coded to both APIs that demonstrates the principles. Windows sockets (WinSock) is specific to Windows and is similar but not identical to BSD. The BSD socket API has been implemented on many platforms including Linux, and is available for embedded systems and real-time operating systems. Socket APIs BSD compatible socket API are available for many platforms; existing TCP/IP application code for UNIX or Linux platforms may be ported with little of no modification. Windows implements the Windows Sockets API. Windows Sockets is based on, but not entirely compatible with BSD 4.3 sockets. Apart from proprietary initialisation and clean-up functions, needed to support the windows environment, there are a number of important differences between BSD sockets and Windows Sockets, the main ones are as follows: Socket descriptors (or handles) in Windows are of type SOCKET, in BSD they are of type int. This is important for a number of reasons, SOCKET is unsigned, and in BSD the value -1 is used to indicate an error. In Windows, the manifest constant INVALID_SOCKET is used to indicate an error. In BSD, socket descriptors and file descriptors are entirely compatible, allowing the file I/O functions such as read(), and write(), to be used with socket streams. In Windows this is not guaranteed to be the case, so the socket specific equivalents such as recv(), and send(), should be used. In the example code I have used socket specific functions for both the BSD and WinSock applications. This keeps things simpler and makes the porting of code between the two platforms simpler. Because the Windows Sockets API is not compatible with the file I/O API, two functions common to both I/O and sockets in BSD have been renamed in Windows. These are close(), and ioctl(), called closesocket(), and ioctlsocket (), respectively in Windows. Code that does not strictly use the FD_XXX macros for accessing fd_set structures will not work correctly with the select() function. Windows Sockets error codes are not available via the errno variable. The function WSAGetLastError() returns socket error codes. BSD error code manifest constants are defined for compatibility, but Microsoft recommend that the WSA prefixed error codes are used instead. Windows Sockets functions return SOCKET_ERROR on error, BSD sockets code commonly check for the value 1 to test for errors. Note that under Windows, using C++, Microsoft Foundation Classes (MFC) provides a wrapper class (Csocket). I shall leave the reader to investigate this, and concentrate on teh C API since this is more universal. COPYRIGHT 2002 - CLIFFORD SLOCOMBE PAGE 3 OF 8
Byte order and structure packing Care must be taken when transferring data between two machines over byte ordering and structure packing, which may differ between different CPU and/or compilers. This means, for example that a multi-byte integer, or a data structure transmitted as a byte stream from one computer, may not be directly read into the same data type on another. To help overcome some of these problems a convention known as network byte order is used. A number of macros are used to convert between the local host byte order and network byte order. The macros impose no overhead on architectures wherethe host byte order matches network byte order. The macros defined in <in.h> are as follows: Macro htonl htons ntohl ntohs Description Convert a long from host to network byte ordering. Convert a short from host to network byte ordering. Convert a long from network to host byte ordering. Convert a short from network to host byte ordering. Differences in byte ordering between the CPU and the network convention may or may not be important for application data. It is however important with respect to network addressing, where the use of the macros is essential to ensure correct addressing and portability of code. Establishing a communication session A connection is initiated an application running on one node on the network, and accepted by another application running on another node (or possibly the same node). Often a client-server relationship is established, where the client requests a connection to the server, and the server can accept one or more simultaneous connections. It is also possible to create peer-peer applications where bothe ends of a connection may either initiate or accept connections. The example considered here is of the client-server form. It is also very simple in that the server may only accept a connection from and service one client at a time. To service multiple clients (a concurrent server), the server application would have to accept a connection and then spawn a separate thread to service the client, before returning to 'listen' for other clients. Since creating multiple threads or tasks is operating system dependent, I have not dealt with it in detail here. Client The steps taken by a client application during a communication session are: Obtain a socket descriptor (handle) by calling the socket() function. Initialise a sockaddr_in structure with addressing data. Connect to the server by calling the connect() function. Send and receive data as necessary by calling the send() and recv() functions as necessary. Alternatively, under BSD socket API the file IO functions read() and write() may be used. Terminate connection by calling close() (BSD) or closesocket() (WinSock). Server The steps taken by a client application during a communication session are: Obtain a server socket descriptor (handle) by calling the socket() function. Initialise a sockaddr_in structure with addressing data. Bind the addressing information to the socket by calling the bind() function. Wait for connection requests on the selected port by calling listen(). COPYRIGHT 2002 - CLIFFORD SLOCOMBE PAGE 4 OF 8
Accept the connection (and get the client socket descriptor) by calling accept(). If the application is a concurrent server, a new thread world be spawned (or an existing thread awoken) to accept the connection before looping back to listen. The new thread would communicate with the client using socket descriptor returned by accept(). Send and receive data as necessary by calling the send() and recv() functions as necessary. Alternatively, under BSD socket API the file IO functions read() and write() may be used. Terminate the client connection by calling close() (BSD) or closesocket() (WinSock) for the client descriptor. When the server terminates, close the server socket by calling close() (BSD) or closesocket() (WinSock) for the client descriptor. A simple example application The application described herein implements a simple TCP/IP application called rprint. It is intended to demonstrate the basics of communicating data via TCP/IP with the minimum of superfluous code. The simplicity is intended to enable the reader to appreciate the essential requirements of TCP/IP applications, without unnecessarily elaborate application code. The rprint application is a client/server application in two parts, a client application, and a server application. For simplicity (and portability), the applications are console (text) mode applications, allowing us to ignorethe added complexity and rigors of GUI applications. The client program is called rprint, it takes an IP address and a port number, and then any number of arbitrary strings as parameters, for example: rprint 10.0.0.1 6000 Hello, world The strings are sent with spaces delimiting them and a NUL terminator to the port at the IP address. The server end is called rprintsvr, and simply prints any NUL terminated string it receives followed by a newline character. The application is very simple, data flows in one direction only, and the server can service exactly one client at a time. The code has been verbosely commented since it is intended to be instructive. It also contains conditional compilation so that it should build on either WinSock or BSD environments. If you intend to develop on only one environment, you could render the code more readable by removing the parts related to the alternate environment. However, writing the code for both platforms in a single source does serve to highlight the differences. Client code for BSD/WinSock / rprint console mode socket client Clifford Slocombe - December 2002 A simple demonstration of a socket client application Sends null terminated string of words passed on the command line to remote server for display. USAGE: rprint <ipaddr> <port> <word> [<word>...] e.g. rprint 10.0.0.1 6000 This is an rprint message. / #include <winsock.h> / Windows Sockets header / #include <socket.h> / BSD sockets library / #include <io.h> / BSD sockets use file I/O / #include <in.h> / network byte order macros / / These error macros are used in Winsock, and are defined here so that the BSD code can be the same. Avoiding lots (more) of messy conditional compilation / #if!defined SOCKET_ERROR #define SOCKET_ERROR -1 #if!defined INVALID_SOCKET #define INVALID_SOCKET -1 #include <string.h> COPYRIGHT 2002 - CLIFFORD SLOCOMBE PAGE 5 OF 8
int main( int argc, charargv ) / Console mode, so main() is OK / WSADATA winsock_info ; / Required by WASStartup, gets filled with information about WinSock implementation / SOCKET sd ; / Socket descriptor (not a file handle in Windows) / SOCKADDR_IN name ; / IP addressing data the term name is a BSD convention / int sd ; / Socket descriptor (file handle in BSD) / struct sockaddr_in name ; / IP addressing data the term name is a BSD convention / int status ; int param ; / WSAStartup is mandatory in WinSock applications / status = WSAStartup( MAKEWORD(1,1), &winsock_info ) ; / No Startup required in BSD, set status to zero so subsequent test suceeds / status = 0 ; if( status == 0 ) / Get a socket descriptor / sd = socket( PF_INET, SOCK_STREAM, 0 ) ; / Test for valid socket... / if( sd!= INVALID_SOCKET ) / Got a good socket, so fill in addressing data / name.sin_family = AF_INET ; / Internet addressing / name.sin_addr.s_addr = inet_addr( argv[1] ) ; / IP address (convert from a.b.c.d string) / name.sin_port = htons( (u_short)atoi(argv[2]) ) ; / Port, (network byte order) / / If IP a.b.c.d address string was valid / if(name.sin_addr.s_addr!= INADDR_NONE ) / Connect to server, (cast address to generic socket address) / status = connect( sd, (struct sockaddr)&name, sizeof(name) ) ; else / Ensure loop will not start if error / status = SOCKET_ERROR ; / For each remaining parameter, or until error... / for( param = 3; status!= SOCKET_ERROR && param < argc; param++ ) / Send parameter string / status = send( sd, argv[param], strlen(argv[param]), 0 ) ; / If no error... / if( status!= SOCKET_ERROR ) / Send a space character / status = send( sd, " ", 1, 0 ) ; / Terminate transmission with a NUL character / (void)send( sd, "\0", 1, 0 ) ; / Close socket with WinSock specific function, BSD close() not guaranteed because descriptor is not a file handle / closesocket( sd ) ; close( sd ) ; / WSACleanup is mandatory in WinSock applications / WSACleanup() ; return( 0 ) ; COPYRIGHT 2002 - CLIFFORD SLOCOMBE PAGE 6 OF 8
Server code for BSD/WinSock / rprintsvr console mode socket server Clifford Slocombe - December 2002 A simple demonstration of a BSD Sockets server application Recieves NUL terminated strings abd displays them. USAGE: rprintsvr port Port parameter must match that used by client / #include <string.h> #include <stdlib.h> #include <stdio.h> #include <winsock.h> / Windows Sockets header / #include <socket.h> / BSD sockets library / #include <io.h> / BSD sockets use file I/O / #include <in.h> / network byte order macros / / These error macros are used in Winsock, and are defined here so that the BSD code can be the same. Avoiding lots (more) of messy conditional compilation / #if!defined SOCKET_ERROR #define SOCKET_ERROR -1 #if!defined INVALID_SOCKET #define INVALID_SOCKET -1 int main( int argc, charargv ) / Console mode, so main() is OK / unsigned int port ; SOCKET server_sd ; / Server socket descriptor / SOCKET client_sd ; / Client socket descriptor / SOCKADDR_IN server_name ; / Server IP addressing / SOCKADDR_IN client_name ; / Client IP addressing / int server_sd ; / Server socket descriptor / int client_sd ; / Client socket descriptor / struct sockaddr_in server_name ; / Server IP addressing / struct sockaddr_in client_name ; / Client IP addressing / int status = SOCKET_ERROR ; int addr_len = sizeof(client_name) ; char c ; / Get port address from command line / port = atoi( argv[1] ) ; / get a server side socket / server_sd = socket( PF_INET, SOCK_STREAM, 0 ) ; / accept() requires pointer to address length so address length must be assigned to a variable / / In BSD valid sockets are positive integers / if( server_sd!= INVALID_SOCKET ) / Got a good socket, so fill in addressing data / memset( &server_name, sizeof(server_name), 0 ) ; server_name.sin_family = AF_INET ; / Internet addressing / server_name.sin_addr.s_addr = INADDR_ANY ; / accept input from any client / server_name.sin_port = htons( port ) ; / but just at this port / / Bind addressing to server socket / status = bind( server_sd, (struct sockaddr)&server_name, sizeof(struct sockaddr) ) ; if( status!= ERROR ) / Wait for activity on bound port, in this case only allowing one pending connection, because this is not a concurrent server / status = listen( server_sd, 1 ) ; while( status!= ERROR ) / accept the connection, binding it to the client descriptor / client_sd = accept( server_sd, (struct sockaddr)&client_name, &addr_len ) ; if( client_sd >= 0 ) / Recieve characters, and display them until a NUL is read / COPYRIGHT 2002 - CLIFFORD SLOCOMBE PAGE 7 OF 8
status = recv( client_sd, &c, 1, 0 ) ; while( c!= '\0' && status!= SOCKET_ERROR ) putchar( (int)c ) ; / display / status = recv( client_sd, &c, 1, 0 ) ; / get next / putchar( '\n' ) ; / Close client socket when done / / Close socket with WinSock specific function, BSD close() not guaranteed because descriptor is not a file handle / closesocket( client_sd ) ; close( client_sd ) ; else status = ERROR ; / Close server socket if it was ever opened / if( server_sd!= SOCKET_ERROR ) / Close socket with WinSock specific function, BSD close() not guaranteed because descriptor is not a file handle / closesocket( server_sd ) ; close( server_sd ) ; return( 0 ) ; Practical applications The example in the section illustrates the essentials of network communication using sockets. In a practical application, it would be likely that multiple clients could be serviced simultaneously, and that data would be exchanged in both directions. Typical server applications incorporate a loop, where the accept is performed. For each accepted connection, a task is either spawned or awoken, the client socket descriptor is passed to the task, so that that client can be serviced, while the server loop is ready to accept new connections. COPYRIGHT 2002 - CLIFFORD SLOCOMBE PAGE 8 OF 8