Concurrency And Server-Side Networking APIs — Part 1

Introduction:

This article explores how a Sun Hotspot JVM behaves when accepting incoming TCP/IP socket connections. The behavior of native server programs–written in C–are also explored to better understand how concurrency can be achieved. Then, a comparison of how these runtime environments implement server-side networking is explored. Finally, a brief introduction to how these concepts are used in the construction of J2EE Container architecture is given.

This article looks at the system calls involved in listening on sockets and accepting incoming connections. Also, how these system calls behave with a single threaded server, multi-threaded server, or multi-process server. Then, switch gears towards how the JVM behaves when multiple Java threads attempt to accept connections from the same server socket. Finally, I discuss the design considerations that need to be taken into account when implementing server-side software with Java.

The Linux 2.6.25 kernel was used to develop and test the programs presented here, but the results should hold true for Solaris and most other Unix variants. The Posix Thread library is used for all multithreading in natively compiled C programs. The GNU GCC compiler is used for compiling all C code. Java 1.5.0_15 is used for all Java examples. The TomCat web container inside of a JBoss v3.2 J2EE container is used to illustrate many threads accepting connections from the same listening end point.

I admit that I am getting away from my usual format where I keep the material on thinkmiddleware.com strictly at the JVM, Java language, and J2EE Container level (or higher). But, there is an important point regarding how the java.net library limits how java.net.ServerSocket objects behave, which I want to capture. To truly capture that point, we’re going a little further down the technology stack than normal.

This article is a rewrite of a brief document I put together during my Masters project in February, 2007.

This article does not present best practices for server-side design architecture. These examples are purely for illustration purposes to demonstrate the point this article is attempting to convey. See [1] & [2] for a thorough discussion of network programing Design Patterns and the Java language.

Networking System Calls involved:

On Posix-compliant operating systems (meaning most Unix-like OSes), the API that developers use to interact with the operating system is called system calls (also see [4]). The Posix specification defines the system calls that must be provided by an OS; this System Call API, implemented on most Unix-like OSes, is guaranteed to not change (or, at least, be backwards compatible). In contrast, the Microsoft Windows operating system doesn’t publish it’s system call API. The concept of a System Call is present, but the API can change from one version of Windows to the next, which it has many times. Microsoft publishes a well-defined API implemented in DLLs called Win32 or the Windows API. Anyone who has ever done any Visual C++ programming on Windows has interacted with this API.

So, under the covers, anytime the JVM, java process, has to communicate over the network, perform I/O, get meta-data about the system, etc, it is makes system calls into the underlying OS kernel.

When a C/C++ developer establishes a socket connection, there are C library routines that wrap system calls. It is the library wrapper functions that are normally used in network programming.

The following system calls (or C-library, wrapper functions) are associated with establishing socket connections.

* socket() — creates an endpoint and returns a file descriptor.
* bind() — assigns the socket (pointed to by a file descriptor) to an address (ip:port).
* listen() — marks the socket as a “passive socket” that is used to accept incoming connections.
* accept() — accept one inbound connection on a file descriptor for which listen() has been previously called.
* connect() — initiate a connection on a socket associated with a file descriptor.
* send()/write() — write data to a socket.
* recv()/read() — read data from a socket.
* close() — close a socket connection

There are many others, but these are the minimum needed to successfully communicate over a network. For example, setsockopt() & getsockopt() can be used to change numerous socket configuration options.

To estabish a server-side socket (a listening endpoint) the following must be called in this order:

*
socket()
*
bind()
*
listen()
*
accept()
*
I/O functions
*
close()

To establish a client-side socket the following must be called in order:

*
socket()
*
connect()
*
I/O functions
*
close()

A Simple Client/Server program: C

To demonstrate network communication in a most redumentary level, a simple client-server application is provided here. Here is the source code for the server; here is the source code for the client.

The server.c file can be compiled with the following command on Linux or Cygwin:

gcc -o server server.c

The client.c program can be compiled with the following command on the same platforms:

gcc -o client client.c

Start the server with the following command:

./server -p 5001

The client can establish a connection to the server with the following command:

./client -hlocalhost -p 5001

This example assumes both client and server are running on the same machine. But, assuming appropriate connectivity, these two programs could communicate from any two points on the Internet.

For our purposes, telnet is a universal client for further examples. Telnet will be used as the client through out the rest of this series.

On the same box, try to connect to the server program with telnet:

telnet localhost 5001

However, the client program referenced in the preceding paragraph could also be used in the rest of the examples.

References

[1] http://www.cs.vu.nl/~mathijs/publications/2002-designpatternsnetworking.ps.gz

[2] http://www.cs.wustl.edu/~schmidt/PDF/lf-PLOPD.pdf

[3] http://www.opengroup.org/onlinepubs/009695399/

[4] http://en.wikipedia.org/wiki/System_call