Concurrent TCP connections A look at design-changes which permit a TCP server to handle multiple clients without delays
Recall design-paradigm socket() bind() listen() accept() read() write() close() socket() bind() connect() write() read() close() The ‘server’ application The ‘client’ application 3-way handshake data flow to server data flow to client 4-way handshake
Three sockets used server’s listening socket server’s connection socket client’s connection socket The ‘server’ processThe ‘client’ process 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’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 socket() bind() listen() accept() read() write() close() socket() connect() write() read() close() Our ‘server’ application Our ‘client’ application 3-way handshake data flow to server data flow to client 4-way handshake Ask user to type in a short sentence and read reply 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
socket() bind() listen() accept() read() write() close() socket() connect() write() read() close() Our ‘server’ application Our ‘client’ application 3-way handshake data flow to server data flow to client 4-way handshake Ask user to type in a short sentence and read reply New ‘echo’ example An indeterminate delay after connection occurs while user types input Ask user to type in a short sentence and read reply 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 $./tcpserver Server is listening on port $./tcpclient2 localhost Please type a short sentence: $./tcpclient2 localhost Please type a short sentence: $./tcpclient2 localhost 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 intsock = socket( AF_INET, SOCK_DGRAM, IPPROTO_TCP ); bind( sock, (sockaddr*)&serveraddr, salen ); listen( sock, 5 ); for(;;){ intconn = accept( sock, (sockaddr*)&clientaddr, &calen ); intpid = fork(); if ( pid == 0 )// child-process { close( sock ); intrx = 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’ server’s listening socket server’s connection socket client’s connection socket The ‘server’ processThe ‘client’ process 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 server’s listening socket server’s connection socket client’s connection socket The ‘server’ parent-process The ‘client’ process 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’s listening socket server’s connection socket The ‘server’ child-process parent closes connection-socket child closes listening-socket
Server continues ‘listening’ server’s listening socket client’s connection socket The ‘server’ parent-process The ‘client’ process 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 server’s connection socket The ‘server’ child-process client’s connection socket Next ‘client’ process
Demo: ‘tcpserver2.cpp’ Execute our revised ‘tcpserver2.cpp’ in one of your windows, and again run our ‘tcpclient2.cpp’ demo in other windows The ‘service delay’ problem has vanished! $./tcpserver2 Server is listening on port $./tcpclient2 localhost Please type a short sentence: $./tcpclient2 localhost Please type a short sentence: $./tcpclient2 localhost Please type a short sentence:
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 child- processes will get released (no zombies!)
Signal-handler #include // for signal() #include // 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