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