Concurrent TCP connections A look at design-changes which multiple clients without delays

advertisement
Concurrent TCP connections
A look at design-changes which
permit a TCP server to handle
multiple clients without delays
Recall design-paradigm
The ‘server’ application
socket()
bind()
The ‘client’ application
socket()
listen()
bind()
accept()
read()
write()
close()
3-way handshake
data flow to server
data flow to client
4-way handshake
connect()
write()
read()
close()
Three sockets used
The server’s ‘listening socket’ is strictly for one-way communication:
it can only receive connection-requests from clients, but it does not
receive a client’s data, nor is it able to send any data back to a client
The ‘server’ process
The ‘client’ process
server’s listening socket
client’s connection socket
server’s connection socket
The server’s ‘connected socket’ is for doing two-way communication:
it can be used by the server to receive data from its connected client,
and it can be used by the server to send data to that connected client
Fast service only
• The design-paradigm we just described is
OK for servers that reply very quickly to a
single client request (as with our original
echo application: a short sentence is sent
by the client, the server capitalizes all its
letters and sends that sentence back, and
then the connection is immediately closed
• But this design-paradigm is not well-suited
for a more general kind of TCP application
Original ‘echo’ example
Our ‘server’ application
socket()
Our ‘client’ application
bind()
Ask user to type in a
short sentence
and read reply
listen()
socket()
accept()
read()
write()
close()
3-way handshake
data flow to server
data flow to client
4-way handshake
connect()
write()
read()
close()
The duration
of this
connection
is very brief
Delayed-service problem
• To demonstrate the problem that arises
with our original “iterative server” design,
we need to make a small revision in our
client’s code – to prolong the duration of
the connection of the server to the client
• If we ‘cut-and-paste’ a few lines of code,
we can arrange for our client to connect to
the server before it reads the user’s input
New ‘echo’ example
Our ‘server’ application
Ask user to type in a
short sentence
and read reply
socket()
bind()
Our ‘client’ application
listen()
accept()
read()
write()
close()
socket()
3-way handshake
connect()
An indeterminate delay
after connection occurs
while user types input
Ask user to type in a
short sentence
and read reply
data flow to server
data flow to client
4-way handshake
write()
read()
close()
cut
and
paste
Demo: ‘tcpclient2.cpp’
• Run our original ‘tcpserver.cpp’ in one of
the windows on your graphical desktop
• Then run our revised ‘tcpclient2.cpp’ demo
in several other windows at the same time
$./tcpclient2 localhost
$./tcpserver
Server is listening on port 54321
Please type a short sentence:
$./tcpclient2 localhost
$./tcpclient2 localhost
Please type a short sentence:
Please type a short sentence:
A ‘concurrent’ server
• To avoid such service delays, most TCP
servers use a different design-paradigm,
taking advantage of UNIX’s multitasking
• Each connection-request that the server
receives will handled by a different task
• The operating system will schedule these
multiple tasks to be executed concurrently,
so delays by one task will not affect others
Server’s code-outline
// the basic steps in an initial ‘concurrent server’ design
int
sock = socket( AF_INET, SOCK_DGRAM, IPPROTO_TCP );
bind( sock, (sockaddr*)&serveraddr, salen );
listen( sock, 5 );
for(;;)
{
int
conn = accept( sock, (sockaddr*)&clientaddr, &calen );
int
pid = fork();
if ( pid == 0 )
// child-process
{
close( sock );
int
rx = read( conn, buf, sizeof( buf ) );
if ( rx > 0 ) write( conn, buf, rx );
close( conn );
exit(0);
}
// parent-process
close( conn );
continue;
}
No change in ‘connection-setup’
The ‘server’ process
The ‘client’ process
server’s listening socket
client’s connection socket
server’s connection socket
The server’s ‘listening socket’ is strictly for one-way communication:
it can only receive connection-requests from clients, but it does not
receive a client’s data, nor is it able to send any data back to a client
Connect, then fork
The ‘server’ parent-process
parent closes connection-socket
server’s listening socket
server’s connection socket
The ‘client’ process
client’s connection socket
The ‘server’ child-process
server’s listening socket
server’s connection socket
child closes listening-socket
The server’s ‘listening socket’ will not used by the child-process, so
it immediately gets closed, and the server’s ‘connection socket’ will
not be used by the parent-process, so it immediately gets closed
Server continues ‘listening’
The ‘server’ parent-process
Next ‘client’ process
server’s listening socket
client’s connection socket
The ‘server’ child-process
The ‘client’ process
client’s connection socket
server’s connection socket
The server’s ‘listening socket’ can continue to receive connection-requests
from other clients that are made to the server’s parent-process, while the
earlier client is maintaining its connection with the server’s child-process
Demo: ‘tcpserver2.cpp’
• Execute our revised ‘tcpserver2.cpp’ in
one of your windows, and again run our
‘tcpclient2.cpp’ demo in other windows
$./tcpclient2 localhost
$./tcpserver2
Server is listening on port 54321
Please type a short sentence:
$./tcpclient2 localhost
$./tcpclient2 localhost
Please type a short sentence:
Please type a short sentence:
• The ‘service delay’ problem has vanished!
New problem: ‘zombies’
• When you use the ‘ps’ command to look at
the list of all of your processes, you notice
that our revised server’s ‘child-processes’
are still residing within the system -- even
though they have already terminated – as
so called ‘zombie’ processes, and they are
using system resources (e.g., memory):
$ ps -a
Parent didn’t ‘wait’
• When a child-process exits, its existence
is remembered within the Linux system
until its parent-process calls one of the
‘wait()’ functions, to find out that child’s
status -- and to relinquish its resources
• Failure to ‘wait’ could eventually exhaust
the system’s memory, preventing further
useful work from being done!
But ‘wait()’ blocks!
• If a parent calls the usual ‘wait()’ function
before its child has terminated, the parent
will be put to sleep, and is awakened only
when one of its child-processes exits
• But putting out server-process to sleep
would delay it from accepting any more
connection-requests from new clients
• To avoid this we need a new mechanism
The SIGCHLD signal
• Whenever a process exits, the operating
system will automatically notify its parent
by delivering a ‘signal’ to the parent
• We can arrange for our concurrent server
to ‘catch’ any such signals, because then
there’s no risk of sleeping if it calls ‘wait()’
• That way, the resources owned by childprocesses will get released (no zombies!)
Signal-handler
#include <signal.h>
#include <wait.h>
// for signal()
// for wait()
void sigchld_action( int signo )
{
wait( NULL );
// release a child-process’s resources
signal( SIGCHLD, sigchld_action );
// reinstall handler
}
int main( int argc, char *argv[] )
{
signal( SIGCHLD, sigchld_action );
…
}
// install handler
In-class exercise
• Try running our ‘tcpserver3.cpp’ example,
which invokes a signal-handler to ‘wait()’
as soon as a child-process calls ‘exit()’
• Now run ‘tcpclient2.cpp’ to satisfy yourself
that a ‘zombie’ process is no longer being
left in the system to consume resources
Download