0% found this document useful (0 votes)
73 views8 pages

TCP Client Design Alternatives

The document discusses various design alternatives for TCP clients and servers in UNIX network programming, including blocking I/O, select(), nonblocking clients, and different server types such as iterative and concurrent servers. It also covers advanced server models like preforked servers with locking mechanisms and descriptor passing techniques for efficient client handling. Each method presents unique trade-offs in complexity, performance, and resource utilization.

Uploaded by

Afreed Shahid
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
73 views8 pages

TCP Client Design Alternatives

The document discusses various design alternatives for TCP clients and servers in UNIX network programming, including blocking I/O, select(), nonblocking clients, and different server types such as iterative and concurrent servers. It also covers advanced server models like preforked servers with locking mechanisms and descriptor passing techniques for efficient client handling. Each method presents unique trade-offs in complexity, performance, and resource utilization.

Uploaded by

Afreed Shahid
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Client/Server Design Alternatives

TCP Client Alternatives


In UNIX network programming, there are multiple methods to design and
implement TCP clients. Each method has its own trade-offs in terms of complexity,
performance, and resource utilization.

There are various alternatives available for TCP client implementation.

1. Basic TCP Client (Blocking I/O)


This is the simplest form of TCP client, where each operation waits to
finish before moving to the next. The client waits for user input, sends it
to the server, and then waits for the reply. It is easy to write but cannot
detect if the server closes the connection during input. This makes it less
reliable in real-time communication.

2. Client Using select()


In this method, the select() system call is used to monitor both user input
and server socket. It improves over the basic method by allowing the
client to detect if the server has disconnected even while waiting for input.
This makes it better suited for interactive programs.

3. Client with shutdown() for Batch Mode


This version is used in batch processing. After sending all data to the
server, the client calls shutdown() to close the write end of the
connection. This informs the server that no more data will be sent,
improving communication flow and efficiency.

4. Nonblocking Client
Here, the client does not wait for I/O operations. It checks if sockets are
ready to read/write and continues. This method is fast and useful in real-
time applications, but writing the logic becomes more complex.

5. Forked and Threaded Clients


Forked clients create two processes (one for sending, one for receiving)
using fork(). Threaded clients use threads instead of processes. Threads
are lighter and more efficient, making threaded clients more suitable for
modern applications with multitasking needs.

TCP Test Client


A TCP Test Client is a sample program used to test the connection and
communication between a client and a server over the TCP. It helps developers
verify whether the server is running, accepting connections, and properly
responding to messages sent from the client side.

1
How it works:

1. Create a socket:
The client starts by creating a socket using the socket() system call,
specifying AF_INET (IPv4) and SOCK_STREAM (for TCP).
2. Specify server details:
The client provides the server’s IP address and port number using a
sockaddr_in structure.
3. Connect to the server:
The client uses the connect() function to establish a TCP connection
with the server.
4. Send data:
Once connected, the client sends a message using send() or write()
to the server.
5. Receive response:
The client waits for a response from the server using recv() or
read().
6. Close connection:
After the communication is complete, the client closes the socket
using the close() system call.

TCP Iterative Server


A TCP Iterative Server is a type of server that handles one client at a time. It
completes the entire interaction with a connected client before moving on to the
next client. This is in contrast with concurrent servers, which can handle multiple
clients simultaneously.

Working Principle: When a client connects to a TCP iterative server

1. The server accepts the connection.


2. It processes the client’s request fully.
3. After finishing, it closes the connection.
4. Then, the server goes back to accept the next client.

TCP Concurrent Server(One Child per Client)

A TCP Concurrent Server is a type of server that can handle multiple clients at
the same time, in parallel. Unlike an iterative server, which processes one
client at a time, a concurrent server creates a new process or thread for every
incoming client connection.

Working Mechanism

The basic steps involved in a TCP concurrent server are:

2
1. Socket creation: The server first creates a TCP socket using the
socket() function.
2. Binding: It binds the socket to a specific IP address and port using
bind().
3. Listening: It starts listening for incoming client connections using
listen().
4. Accepting connections: It waits for client connections using
accept().
5. Forking: Upon accepting a connection, the server uses fork() to
create a new child process.
6. Serving the client: The child process handles all communication
with the client (reading and writing).
7. Closing sockets:
o The child closes the listening socket and serves the client.
o The parent closes the connected socket and loops back to
accept another connection.

This way, each client is handled by a separate process and


communication with multiple clients occurs in parallel.

Code Structure

for ( ; ; ) {
connfd = accept(listenfd, ...); // Accept a client connection

if ((pid = fork()) == 0) { // Child process


close(listenfd); // Child doesn't need listening socket
handle_client(connfd); // Function to serve the client
close(connfd); // Close connection
exit(0); // End child process
}

close(connfd); // Parent doesn't need connected socket


}

TCP preforked server, no locking around accept

A TCP preforked server creates multiple child processes in advance before any client
connects. These processes are ready to handle clients immediately, avoiding the
overhead of creating a new process for each client.

Shared Listening Socket: After starting, the server creates a socket, binds it to
an address and port, and begins listening. It then uses fork() to create multiple
child processes in advance. Each child inherits the same listening socket and waits
in a loop by calling accept(), blocking until a client connects.

3
Handling Client Connections: When a client connects, all children blocked on
accept() are awakened. Only one child succeeds in accepting the connection, while
the others go back to waiting. This works because the socket’s internal structures
are shared between processes.

Platform Dependency: This model works correctly on BSD-like systems, where


accept() is atomic in the kernel. On some System V systems, accept() is a library
function, and multiple children might accept the same connection, causing errors.

Thundering Herd Problem: All children waking up for one client causes
unnecessary CPU usage. This is known as the thundering herd problem, which
affects performance when many children are blocked on accept().

TCP preforked server, file locking around accept

When a server uses multiple preforked child processes, each process must wait
to accept client connections. However, on some systems (like older System V
Release 4), multiple processes calling accept() at the same time can cause
errors because accept() is not atomic on such systems.

To prevent this, we place a file lock around the accept() call, ensuring that
only one process can call accept() at a time.

Working of the Server

1. Preforking Children:
The server starts by creating the listening socket and then forks
several child processes, all of which inherit the socket.

4
2. Lock Initialization:
A temporary lock file is created using my_lock_init. It uses the
fcntl() system call to set up file locking.
3. Locking Mechanism:
In each child, before calling accept(), the process acquires the lock
using my_lock_wait().After accepting a client, it releases the lock
using my_lock_release().

This locking ensures that only one child enters the accept() system call, while
others wait for the lock. This avoids protocol errors and ensures correct
behavior on platforms where accept() isn’t safe across processes.

TCP preforked server, thread locking around accept

In this model, the server forks multiple child processes in advance. All child
processes share the same listening socket. However, to prevent errors caused
by multiple processes calling accept() at the same time, thread-based mutex
locking is used around the accept() call.

Instead of using file locks (which are slower), we place a Pthread mutex in
shared memory. This mutex is configured with the PTHREAD_PROCESS_SHARED
attribute, allowing it to be shared across different processes.

Each child process:

1. Locks the mutex before calling accept(),


2. Accepts the client connection, and then
3. Releases the mutex.

Shared memory is created using mmap() with /dev/zero. The mutex ensures that
only one child enters accept() at a time, preventing race conditions and
improving performance over file locks.

This approach is faster than file locking and works well on systems that
support process-shared mutexes.

TCP preforked server, descriptor passing

In this model, the server creates multiple child processes in advance. But
unlike other preforked servers, only the parent process accepts client
connections. After accepting a client, the parent passes the connected socket
descriptor to a child process using a method called descriptor passing.

1. Server Setup

5
The server starts by creating a TCP listening socket using socket(), bind(),
and listen(). It then creates multiple child processes in advance using
fork(). These children are ready to handle clients but do not call accept()
themselves.

2. Only Parent Calls accept()

In this model, only the parent process accepts client connections using
the accept() system call. Child processes wait until the parent assigns
them a client to handle.

3. Use of Stream Pipes

To pass the connected client socket to a child, the server uses stream
pipes, which are created using the socketpair() function. These are Unix
domain socket pairs that allow communication between the parent and
each child process.

4. Descriptor Passing

When a client connects, the parent accepts the connection and uses
write_fd() to send the socket descriptor through the stream pipe to an
available child. The child receives the descriptor using read_fd(). This
technique is known as descriptor passing and allows a process to send
an open file/socket descriptor to another process.

5. Child Handles the Client

Once the child receives the descriptor, it communicates with the client
(reads, writes, etc.). After completing the client request, the child sends a
6
small message (e.g., a byte) back to the parent through the pipe to
indicate that it is now free.

6. No Locking and Better Control

Since only the parent handles accept(), there is no need for locking or
mutex around the accept() call. This also avoids the thundering herd
problem. The parent has full control to assign connections to children and
can also monitor which child is busy or free.

TCP concurrent server, one thread per client

A TCP concurrent server with one thread per client is a multithreaded server
model. Instead of creating a new process for each client like the traditional
fork() model, it creates a new thread for every incoming client connection.

1. Server Initialization

The server begins by creating a listening socket using socket(), binds it to


a port using bind(), and starts listening for connections using listen().

2. Accepting Client Connections

The server continuously calls accept() to handle new client connections.


Each call returns a new socket descriptor representing the connection with
a client.

3. Creating a Thread per Client

For each client, the server uses pthread_create() to start a new thread.
This thread handles all communication with that specific client. The
connected socket is passed as an argument to the thread function.

4. Thread Detachment

The thread usually detaches itself using pthread_detach(). This means the
system will automatically clean up resources after the thread finishes,
preventing memory leaks.

5. Client Communication

Inside the thread, the server performs reading, processing, and writing to
the client. After the task is done, the thread closes the socket and exits.

6. Shared Memory and Synchronization

7
Since all threads are part of the same process, they share memory. If
multiple threads access shared data, synchronization techniques like
mutexes are used to avoid data corruption.

Common questions

Powered by AI

A TCP preforked server using descriptor passing avoids the thundering herd problem because only the parent process calls accept(), thereby preventing multiple processes from being awakened for a single client connection. By ensuring only one process—the parent—handles connection acceptance and assigns it to a child via descriptor passing, system resources are conserved and server performance is improved. This approach allows the parent to have complete control over client assignment, avoiding redundant wake-ups of all processes and improving efficiency .

The thundering herd problem arises when multiple child processes or threads are awakened simultaneously to handle a single client connection, leading to unnecessary CPU usage and resource competition. It is a significant challenge in TCP server models that use preforking without coordination. To mitigate this problem, different server configurations employ locking mechanisms, such as file locks or mutexes, around the accept() call to ensure only one process or thread can accept a connection at any time. Additionally, using a single parent process for accept() with descriptor passing avoids the problem entirely by only waking up the parent process to handle new connections .

File locking in TCP preforked servers ensures that only one child process at a time can call accept(), helping to prevent protocol errors on systems where accept() is not atomic. However, file locking can be slower due to disk I/O overhead. Mutex locking, particularly with PTHREAD_PROCESS_SHARED, provides a faster alternative because it occurs in memory rather than on disk, benefiting systems that support process-shared mutexes. While file locking ensures broad compatibility, mutex locking enhances performance significantly on supported systems .

The thread per client model differs from the preforked process model by using threads instead of processes to handle client connections. This means all threads run within the same process and share the same memory space, leading to higher efficiency due to lower overhead than creating new processes. However, this necessitates careful thread synchronization to prevent data corruption because all threads have access to shared data. Synchronization techniques like mutexes are required to manage access to shared resources safely, a concern not present with separate processes that have isolated memory spaces .

The main difference between forked and threaded clients is that forked clients create separate processes, each with its own memory space, while threaded clients use threads within the same process, sharing the same memory space. In modern applications, threaded clients are often preferred because threads are more lightweight and consume fewer resources compared to processes. Additionally, threads allow more efficient multitasking within the same application by avoiding the overhead of inter-process communication necessary in forked clients .

Using the select() system call in a TCP client allows the program to monitor multiple file descriptors, including user input and server sockets. This enables the client to check for inputs from various sources (e.g., user input or server responses) and also detect server disconnections, even when waiting for input. This approach improves on the basic blocking I/O method by enabling the client to handle interactive programs more efficiently, providing better real-time response and reliability .

A TCP preforked server employing thread locking around accept() using mutexes offers superior performance compared to file locks due to reduced overhead and faster in-memory operations. Thread locking requires system support for PTHREAD_PROCESS_SHARED to allow mutexes to be shared across processes. Implementing thread locking involves setting up shared memory using mmap() and properly configuring the mutex with PTHREAD_PROCESS_SHARED. This configuration reduces latency and increases throughput compared to file locks, enhancing server performance on platforms that support these features .

A basic TCP client with blocking I/O is simple to implement since each operation finishes before the next one starts, making it straightforward to manage flow control and operations. However, its main disadvantage is that it cannot detect if the server closes the connection during input. This limits its reliability, especially in real-time communication scenarios. Additionally, it can lead to inefficient CPU usage since the client is idle while waiting for operations to complete .

A TCP concurrent server manages multiple client connections by creating a separate process or thread for each new client using fork() or thread creation. This approach allows the server to handle multiple clients simultaneously, enhancing scalability as each client is served independently of others. By using child processes, the server can isolate each client's resources, while threading offers even greater efficiency and resource sharing. These strategies enable the server to process large numbers of connections in parallel, making it highly scalable and responsive to many users .

A nonblocking client optimizes real-time application performance by allowing continuous operation without waiting for I/O operations to complete before proceeding. Unlike blocking designs, nonblocking clients can initiate multiple actions simultaneously, checking sockets for readiness before performing read/write operations. This enhances responsiveness and throughput since the client can process incoming/outgoing data immediately as it becomes available. However, it requires more complex logic to handle operations efficiently and may introduce greater complexity in error handling compared to simpler blocking designs .

You might also like