0% found this document useful (0 votes)
553 views54 pages

Chapter05 TCP Client Server Example

The document describes a TCP client/server example that implements an echo service. It includes code for a TCP echo server and client that connect, exchange data, and terminate normally. The summary explores various scenarios like normal startup and termination, signal handling on termination, and handling zombie processes. Code snippets are provided for key functions in the server and client like connecting, reading/writing data, and setting up signal handlers. The document aims to provide a simple but complete TCP client/server example to demonstrate network programming concepts.
Copyright
© Attribution Non-Commercial (BY-NC)
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)
553 views54 pages

Chapter05 TCP Client Server Example

The document describes a TCP client/server example that implements an echo service. It includes code for a TCP echo server and client that connect, exchange data, and terminate normally. The summary explores various scenarios like normal startup and termination, signal handling on termination, and handling zombie processes. Code snippets are provided for key functions in the server and client like connecting, reading/writing data, and setting up signal handlers. The document aims to provide a simple but complete TCP client/server example to demonstrate network programming concepts.
Copyright
© Attribution Non-Commercial (BY-NC)
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

Unix Network Programming

5. TCP Client/Server Example


1

Concept
5.1 Introduction 5.2 TCP Echo Server: main Function 5.3 TCP Echo Server: str_echo Function 5.4 TCP Echo Client: main Function 5.5 TCP Echo Client: str_cli Function 5.6 Normal Startup 5.7 Normal Termination 5.8 POSIX Signal Handling 5.9 Handling SIGCHLD Signals 5.10 wait and waitpid Functions 5.11 Connection Abort before accept Returns 5.12 Termination of Server Process 5.13 SIGPIPE Signal 5.14 Crashing of Server Host 5.15 Crashing and Rebooting of Server Host 5.16 Shutdown of Server Host 5.17 Summary of TCP Example 5.18 Data Format 5.19 Summary
2

5.1 Introduction
Echo Client & Server
stdin stdout fgets fputs TCP client writen readline readline writen TCP client

Figure 5.1 Simple echo client and server

The client reads a line of text from its standard input and writes the line to the server. The server reads the line from its network input and echoes the line back to the client. The client reads the echoed line and prints it on its standard output

5.1 Introduction
Implementation of an echo server
A client/server that echoes input lines is a valid, yet simple, example of a network application.

Besides running out client and server in their normal mode, we examine lots of boundary conditions for this example:
what happens:
when the client and server are started. when the client terminates normally. to the client if the server process terminates before the client is done. to the client if the server host crashes. and so on
4

5.2 TCP Echo Server: main Function


#include "unp.h" "unp.h" int main(int argc, char **argv) argc, **argv) { int listenfd, connfd; listenfd, connfd; pid_t childpid; childpid; socklen_t clilen; clilen; struct sockaddr_in cliaddr, servaddr; cliaddr, servaddr; listenfd = Socket(AF_INET, SOCK_STREAM, 0); Socket(AF_INET, bzero(&servaddr, sizeof(servaddr)); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); SERV_PORT); htons( Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); Bind(listenfd, &servaddr, sizeof(servaddr)); Listen(listenfd, LISTENQ); Listen(listenfd, for ( ; ; ) { clilen = sizeof(cliaddr); sizeof(cliaddr); connfd = Accept(listenfd, (SA *) &cliaddr, &clilen); Accept(listenfd, &cliaddr, &clilen); if ( (childpid = Fork()) == 0) { /* child process */ (childpid Fork()) Close(listenfd); /* close listening socket */ ); Close(listenfd str_echo(connfd); /* process the request */ str_echo(connfd); exit(0); } Close(connfd); /* parent closes connected socket */ Close(connfd); } }

unp.h 9877 5001~49151

5.3 TCP Echo Server: str_echo Function


#include "unp.h" void str_echo(int sockfd) { ssize_t n; char buf[MAXLINE]; again: while ( (n = read(sockfd, buf, MAXLINE)) > 0) Writen(sockfd, buf, n); if (n < 0 && errno == EINTR) goto again; else if (n < 0) err_sys("str_echo: read error"); }

5.4 TCP Echo Client: main Function


#include "unp.h" "unp.h" int main(int argc, char **argv) argc, **argv) { int sockfd; sockfd; struct sockaddr_in servaddr; servaddr; if (argc != 2) (argc err_quit("usage: tcpcli <IPaddress>"); err_quit("usage: IPaddress>"); sockfd = Socket(AF_INET, SOCK_STREAM, 0); Socket(AF_INET, bzero(&servaddr, sizeof(servaddr)); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_family AF_INET; servaddr.sin_port = htons(SERV_PORT); servaddr.sin_port htons(SERV_PORT); Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); Inet_pton(AF_INET, argv[1], servaddr.sin_addr); Connect(sockfd, (SA *) &servaddr, sizeof(servaddr)); Connect(sockfd, &servaddr, sizeof(servaddr)); str_cli(stdin, sockfd); str_cli(stdin, sockfd); exit(0); } /* do it all */

5.5 TCP Echo Client: str_cli Function


#include "unp.h" void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; while (Fgets(sendline, MAXLINE, fp) != NULL) { Writen(sockfd, sendline, strlen(sendline)); if (Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout); } }


Server Accept
SY N

Client

Connect wait_for_connect

wait_for_connect

ACK, SYN

A CK

read tcp_data_wait
D A TA
ACK

write ESTABLISHED

ESTABLISHED

5.6 Normal Startup (1/4)


10

5.6 Normal Startup (2/4)


LISTEN

11

5.6 Normal Startup (3/4)


12

5.6 Normal Startup (4/4)


/

13

5.7 Normal Termination (1/2)


Normal termination of client and server
1. 2.

3.

4.

5. 6.

7.

EOF (control+D) , fgets null , str_cli . main str_cli exit . , descriptor close, close. FIN , ACK . , CLOSE_WAIT , FIN_WAIT_1 . FIN , readline 0 , str_cli Child main exit . , descriptor close, TCP TIME_WAIT . , SIGCHLD .

SIGCHLD .

14

5.7 Normal Termination (2/2)


15

5.8 POSIX Signal Handling (1/6)


POSIX (Portable Operating System Interface)? . .

16

5.8 POSIX Signal Handling (2/6)


A signal is a notification to a process that an event has occurred. Signals are sometimes called software interrupts. Signals usually occur asynchronously.
By this we mean that a process doesnt know ahead of time exactly when a signal will occur.

Signals can be sent


By one process to another process (or to itself) By the kernel to a process

17

5.8 POSIX Signal Handling (3/6)


The SIGCHLD signal that we described at the end of the previous section is one that is sent by the kernel whenever a process terminates, to the parent of the terminating process. Every signal has a disposition, which is also called the action is called the action associated with the signal.

18

5.8 POSIX Signal Handling (4/6)


We set the disposition of a signal by calling the sigaction function and we have three choices for the disposition:
We can provide a function that is called whenever a specific signal occurs. This function is called a signal handler and this action is called catching a signal. The two signals SIGKILL and SIGSTOP cannot be caught.
Out function is called with a signal integer argument that is the signal number and the function returns nothing. Its function prototype is therefore
void handler(int signo);

We can ignore a signal by setting its disposition to SIG_IGN. The two signals SIGKILL and SIGSTOP cannot be ignored. We can set the default disposition for a signal by setting its disposition to SIG_DFL. The default is normally to terminate a process on receipt of a signal, with certain signals also generating a core image of the process in its current working directory.
19

5.8 POSIX Signal Handling (5/6)


signal Function
The POSIX way to establish the disposition of a signal is to call the sigaction function. An easier way to set the disposition of a signal is to call the signal function. But, signal is an historical function that predates POSIX. The solution is to define our own function named signal that just calls the POSIX sigaction function. This provides a simple interface with the desired POSIX semantics.
20

5.8 POSIX Signal Handling (6/6)


signal function that calls the POSIX sigaction function
#include "unp.h" "unp.h" Sigfunc * signal(int signo, Sigfunc *func) signo, func) { struct sigaction act, oact; oact; act.sa_handler = func; act.sa_handler func; sigemptyset(&act.sa_mask); sigemptyset(&act.sa_mask); act.sa_flags = 0; act.sa_flags if (signo == SIGALRM) { (signo SIGALRM) #ifdef SA_INTERRUPT act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */ act.sa_flags #endif } else { #ifdef SA_RESTART act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */ act.sa_flags #endif } if (sigaction(signo, &act, &oact) < 0) (sigaction(signo, &oact) return(SIG_ERR); return(SIG_ERR); return(oact.sa_handler); return(oact.sa_handler); }

void (*signal(int signo, void (*func)(int)))(int);

typedef void Sigfunc(int); Sigfunc


Sigfunc *signal(int signo, Sigfunc *func);

21

5.9 Handling SIGCHLD Signals (1/4)


The purpose of the zombie state is to maintain information about the child for the parent to fetch at some later time.
process ID of the child, termination status, information on the resource utilization of the child(CPU time, memory, etc.).

If a process terminates, and that process has children in the zombie state, the parent process ID of all the zombie children is set to 1(the init process), which will inherit the children and clean them up(i.e., init will wait for them, which removes the zombie).
22

5.9 Handling SIGCHLD Signals (2/4)


Handling zombies
Obviously we do not want to leave zombies around. They take up space in the kernel and eventually we can run out of process. Whenever we fork children, we must wait for them to prevent them from becoming zombies. To do this, we establish a signal handler to catch SIGCHLD, and within the handler, we call wait.

23

5.9 Handling SIGCHLD Signals (3/4)


Version of SIGCHLD signal handler that calls wait
#include "unp.h" void sig_chld(int signo) { pid_t pid; int stat; pid = wait(&stat); printf("child %d terminated\n", pid); return; }

24

5.9 Handling SIGCHLD Signals (4/4)


zombie

25

5.10. wait and waitpid Functions (1/6)


In Figure 5.7, we called the wait function to handle the terminated child. Difference between wait and waitpid
include <sys/wait.h> pid_t wait(int *statloc); pid_t waitpid(pid_t pid, int *statloc, int options);

26

5.10. wait and waitpid Functions (2/6)


wait Function
If there are no terminated children for the process calling wait, but the process has one or more children that are still executing, then wait blocks until the first of the existing children terminates.

waitpid Function
waitpid gives us more control over which process to wait for and whether or not to block.
First, the pid argument lets us specify the process ID that we want to wait to wait for.
A value of -1 says to wait for the first of our children to terminate.

The options argument lets us specify additional options.


The most common option is WNOHANG.

27

5.10. wait and waitpid Functions (3/6)

28

5.10. wait and waitpid Functions (4/6)


It is this delivery of multiple occurrences of the same signal that causes that problem we about to see.

29

5.10. wait and waitpid Functions (5/6)


Final (correct) version of sig_chld function that calls waitpid.
#include "unp.h"

void sig_chld(int signo) { pid_t pid; int stat; while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) printf("child %d terminated\n", pid); return; }

30

5.10. wait and waitpid Functions (6/6)


The purpose of this section has been to demonstrate three scenarios that we can encounter with network programming.
1.

2.

3.

We must catch the SIGCHLD signal when forking child processes. We must handle interrupted system calls when we catch signals. A SIGCHLD handler must be coded correctly using waitpid to prevent any zombies from being left around.
31

5.11 Connection Abort before accept Returns (1/2)

Receiving an RST for an ESTABLISHED connection before accept is called.

32

5.11 Connection Abort before accept Returns (2/2)

.
Berkeley-derived implementations: , . SVR4 implementations: EPROTO(protocol error) errno . POSIX: ECONNABORTED .

33

5.12 Termination of Server Process (1/10)


We will now start our client/server and kill the server child process. This simulates the crashing of the server process, so we can see what happens to the client.

34

5.12 Termination of Server Process (2/10)


Step 1
Start the server and client and type one line to the client to verify that all is okay.

Server (tcpserv04.c)

Client (tcpcli04.c)
35

5.12 Termination of Server Process (3/10)


Step 2
Find the process ID of the server child and kill it.

36

5.12 Termination of Server Process (4/10)


Step 3
The SIGCHLD signal is sent to the server parent and handled correctly.

37

5.12 Termination of Server Process (5/10)


Step 4
Nothing happens at the client. The client TCP receives the FIN from the server TCP and responds with an ACK, but the problem is that the client process is blocked in the call to fgets waiting for a line from the terminal.

38

5.12 Termination of Server Process (6/10)


Step 5
Running netstat at this point shows the stat of the sockets.

Client (tcpcli04.c)

Server (tcpserv04.c)
39

5.12 Termination of Server Process (7/10)


Step 6 (1/2)
We can still type a line of input to the client. Here is what happens at the client starting from Step 1:

40

5.12 Termination of Server Process (8/10)


Step 6 (2/2)
When we type hi, str_cli calls writen and the client TCP sends the data to the server. This is allowed by TCP because the receipt of the FIN by the client TCP only indicates that the server process has closed its end of the connection and will not be sending any more data. The receipt of the FIN does not tell the client TCP that the server process has terminated. When the server TCP receives the data from the client, it response with an RST since the process that had that socket open has terminated.
41

5.12 Termination of Server Process (9/10)


Step 7
The client process will not see the RST because it calls readline immediately the FIN that was received in Step 2. Our client is not expecting to receive an EOF at this point so it quits with the error message server terminated prematurely.

42

5.12 Termination of Server Process (10/10)


Step 8
When the client terminates, all its open descriptors are closed.

The problem in this example is that the client is blocked in the call to fgets when the FIN arrives on the socket. The client is really working with two descriptors-the socket and the user input-and instead of blocking on input from only one of the two sources, it should block on input from either source.
43

5.13 SIGPIPE Signal (1/4)


What happens if the client ignores the error return from readline and writes more data to the server?
For example, if the client needs to perform two writes to the server before reading anything back, with the first write eliciting the RST.

The rule that applies is:


When a process writes to a socket that has received an RST, the SIGPIPE signal is sent to the process. The default action of this signal is to terminate the process. So the process must catch the signal to avoid being involuntarily terminated.
44

5.13 SIGPIPE Signal (2/4)


To see what happens with SIGPIPE, we modify our client as shown in this code.
#include "unp.h" "unp.h" void str_cli(FILE *fp, int sockfd) fp, sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; sendline[MAXLINE], recvline[MAXLINE]; while (Fgets(sendline, MAXLINE, fp) != NULL) { (Fgets(sendline, fp) Writen(sockfd, sendline, 1); Writen(sockfd, sendline, sleep(1); Writen(sockfd, sendline+1, strlen(sendline)-1); Writen(sockfd, strlen(sendline)if (Readline(sockfd, recvline, MAXLINE) == 0) (Readline(sockfd, recvline, err_quit("str_cli: server terminated prematurely"); err_quit("str_cli: Fputs(recvline, stdout); Fputs(recvline, stdout); } }

45

5.13 SIGPIPE Signal (3/4)


If we run the client on our Linux host, we get:
linux % tcpcli11 [Link] hi there hi there bye Broken pipe
46

5.13 SIGPIPE Signal (4/4)

47

5.14 Crashing of Server Host


This scenario will test to see what happens when the server host crashes.
1.

2.

3.

4.

When the server host crashes, nothing is sent out on the existing network connections. We type a line of input to the client, it is writen by writen, and is sent by the client TCP as a data segment. The client then blocks in the call to readline, waiting for the echoed reply. We will see the client TCP continually retransmitting the data segment, trying to receive an ACK from the server. When the client TCP fanally gives up, an error is returned to the client process.
48

5.15 Crashing and Rebooting of Server Host


In this scenario, we will establish a connection between the client and server and then assume the server host crashes and reboots.
1. 2. 3.

4.

5.

Start the server and then the client. The server host crashes and reboots. Type a line of input to the client, which is sent as a TCP data segment to the server host. When the server host reboots after crashing, its TCP loses all information about connections that existed before the crash. Therefore, the server TCP responds to the received data segment from the client with an RST. Client is blocked in the call to readline when the RST is received, causing readline to return the error ECONNRESET.

49

5.16 Shutdown of Server Host



init SIGTERM , (5~20) . SIGKILL . SIGTERM SIGKILL . select poll .
50

5.17 Summary of TCP Example


51

5.17 Summary of TCP Example


52

5.18 Data Format



#include "unp.h" "unp.h" void str_echo(int sockfd) sockfd) { long arg1, arg2; ssize_t n; char line[MAXLINE]; line[MAXLINE]; for ( ; ; ) { if ( (n = Readline(sockfd, line, MAXLINE)) == 0) Readline(sockfd, return; /* connection closed by other end */ if (sscanf(line, "%ld%ld", &arg1, &arg2) == 2) (sscanf(line, "%ld%ld", snprintf(line, sizeof(line), "%ld\n", arg1 + arg2); snprintf(line, sizeof(line), "%ld\ else snprintf(line, sizeof(line), "input error\n"); snprintf(line, sizeof(line), error\ n = strlen(line); strlen(line); Writen(sockfd, line, n); Writen(sockfd, } }

53

5.18 Data Format



#include "unp.h" "unp.h" #include "sum.h" "sum.h" void str_cli(FILE *fp, int sockfd) fp, sockfd) { char sendline[MAXLINE]; sendline[MAXLINE]; struct args args; args; struct result result; result; while (Fgets(sendline, MAXLINE, fp) != NULL) { (Fgets(sendline, fp) if (sscanf(sendline, "%ld%ld", &args.arg1, &args.arg2) != 2) { (sscanf(sendline, "%ld%ld", printf("invalid input: %s", sendline); sendline); continue; } Writen(sockfd, &args, sizeof(args)); Writen(sockfd, &args, sizeof(args)); if (Readn(sockfd, &result, sizeof(result)) == 0) (Readn(sockfd, sizeof(result)) err_quit("str_cli: server terminated prematurely"); err_quit("str_cli: printf("%ld\n", [Link]); printf("%ld\ [Link]); } }

54

Common questions

Powered by AI

The historical signal function provides a simpler interface compared to sigaction, making it easier to set the disposition of a signal in a straightforward way . However, it has limitations such as not being consistent across different systems, which is why it pre-dates POSIX standards. The sigaction function, while more complex, offers more robust features like reliable signal handling across different UNIX systems and more control over the signal handling process through its ability to manage both the action and set of masked signals . POSIX established sigaction to replace the historical signal for these reasons, prioritizing interoperability and consistent behavior .

Handling SIGCHLD differs in that it specifically concerns the termination of child processes, which is crucial for parent processes to prevent the existence of zombie processes. In networking applications, where multiple child processes can be spawned to handle separate client requests, using SIGCHLD to catch termination signals lets the server determine when a child process has completed its task and proceed to clean up resources effectively, maintaining system performance . Other signals might not have this direct role in managing the child process lifecycle, which is why SIGCHLD handling – often involving the wait call – is distinctive and vital in network-programmed server/client models .

POSIX signals play a critical role in Unix network programming by allowing asynchronous communication between processes and the operating system. When a child process terminates, the SIGCHLD signal is sent to the parent process, which informs it of the child's termination. The parent process can handle this signal by establishing a signal handler that calls the wait function to clean up and prevent 'zombie' processes that still occupy space in the kernel . By doing so, the system avoids running out of process slots, maintaining efficiency and reliability .

Server shutdown in TCP/IP networking is a critical procedure that involves closing connections gracefully to ensure data integrity and notify clients of the termination. When a server host shuts down, it initiates by sending a FIN to all connected clients to signal the end of data transmission from the server side. The clients respond with an ACK, indicating acknowledgment of the shutdown signal. The server doesn’t send further data but may still receive data from clients until they acknowledge the closure, allowing for orderly disconnection. Mismanagement of this process can lead to dropped packets or abrupt connection closures, disrupting dependent client operations and potentially causing data loss . Proper shutdown ensures all communication is complete and both sides are appropriately informed of the connection's status before termination .

Signal disposition in UNIX significantly impacts process management by determining how processes react to various events, potentially affecting system states and process longevity. This disposition can be set to one of several actions: catching with a custom signal handler, ignoring the signal, or setting it to default disposition, which usually terminates the process . Proper handling of signals like SIGCHLD, through catching and invoking wait or handling clean up, prevents the creation of zombie processes and ensures efficient resource utilization by allowing parent processes to react appropriately to changes in child processes' states .

Ignoring signals, particularly SIGPIPE, can lead to unhandled scenarios where a process tries to write to a closed socket, typically resulting in abrupt process termination by default. This can lead to unexpected terminations and loss of related process data, especially relevant in networked systems like client-server models where connections might close unexpectedly. To mitigate these challenges, processes should appropriately handle these signals by installing signal handlers that provide graceful error handling and preventing crashing of the process, thereby ensuring that the application maintains stability and recovers from errors or unexpected conditions suitably . Using signal handlers like sigaction allows programs to specify functions to execute upon receiving signals, avoiding default behaviors that might interrupt process flow without adequate recovery mechanisms .

The TIME_WAIT state is crucial in TCP networking as it ensures that any delayed packets are dealt with appropriately and provides a buffer period before a closed connection can reopen. After a child process that handled a TCP connection terminates, the socket enters TIME_WAIT to prevent potential confusion from delayed duplicates of packets of the closed session. This state also ensures that the final acknowledgment of a connection closure has been properly received by both parties, preventing premature reopening and reuse of the socket before the network has been appropriately cleared, thereby maintaining stability and reliability in the TCP/IP protocol .

Handling SIGPIPE signals is necessary because if a process tries to write to a socket that has been closed by the peer, the operating system sends a SIGPIPE signal to the writing process by default, which usually results in process termination. To manage this, the program can catch this signal using a signal handler or ignore it by setting its disposition to SIG_IGN. However, ignoring it without proper handling could allow attempts to write to a closed socket without any notification, so most implementations prefer to handle it using a signal handler .

When a server process terminates unexpectedly, it closes its end of the connection, prompting TCP to send a FIN signal, which the client TCP receives as an indication to stop receiving data from the server side . This does not necessarily mean the server process has terminated, as the client may still continue sending data until a response is attempted. Upon attempting to send data, if the server truly terminated, the server-side TCP cannot acknowledge it, causing the client to receive a TCP reset (RST) signal. This informs the client that no further communications will be possible with the server process .

In a TCP echo server, if the server terminates while the client is blocked waiting for input, the client can handle this by modifying its routine to wait for input from multiple sources, such as both the user and the network. Instead of using a single blocking call like fgets only on user input, the client can employ a mechanism that waits for input from either the terminal or the network. This way, the client detects the server's termination signal promptly and can respond accordingly, preventing a blocked state .

You might also like