Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
iakovlev.org

 W. R. Stevens  : Глава 5

Пример TCP-соединения клиент-сервер

Напишем простой пример клиент-сервер . Эхо-сервер будет функционировать следующим образом :
1. Клиент считывает строку ввода и отправляет ее на сервер
2. Сервер считывает ее и отсылает обратно
3. Клиент читает отраженную строку и выводит ее
Здесь fgets , fputs - стандартные функции , writen , readline - врапперы , специально написанные Стивенсом.
В примере будут жестко прописаны ip-шники и порты.

Сервер

Это файл tcpserv01.c - исполняемый файл будет называться server :

 #include	"unp.h"
 
 int main(int argc, char **argv)
 {
 	int					listenfd, connfd;
 	pid_t				childpid;
 	socklen_t			clilen;
 	struct sockaddr_in	cliaddr, servaddr;
 
 	listenfd = Socket(AF_INET, SOCK_STREAM, 0);
 
 	bzero(&servaddr, sizeof(servaddr));
 	servaddr.sin_family      = AF_INET;
 	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
 	servaddr.sin_port        = htons(SERV_PORT);
 
 	Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
 
 	Listen(listenfd, LISTENQ);
 
 	for ( ; ; ) {
 		clilen = sizeof(cliaddr);
 		connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
 
 		if ( (childpid = Fork()) == 0) {	/* child process */
 			Close(listenfd);	/* close listening socket */
 			str_echo(connfd);	/* process the request */
 			exit(0);
 		}
 		Close(connfd);			/* parent closes connected socket */
 	}
 }
 
Яковлев С : Все функции , которые начинаются с большой буквы - это врапперы , или обертки , написанные специально Стивенсом для удобочитаемости и простоты кода . Стивенс собрал их в одну кучу в каталоге lib. Для компиляции примеров из 5-й главы мы не будем тянуть всю либу , а возьмем только ее часть.

Вначале создаем сокет . В адрес сокета пишем универсальный адрес - INADDR_ANY и номер заранее известного порта - SERV_PORT , мы его прописываем в хидере unp.h и он равен 9877. Мы изначально берем номер порта , который больше , чем 1023 - нам не нужен зарезервированный порт. Он также больше 5000 - для того , чтобы не было конфликта с динамическими портами. Он меньше 49152 - опять же для того , чтобы избежать конфликта с "правильным" диапазоном динамических портов. Сокет становится прослушивающим после функции listen. Далее сервер блокируется после вызова accept , ожидая подключения клиента. Для каждого клиента функция fork порождает дочерний процесс , который и обслуживает этого клиента. Дочерний процесс закрывает прослушивающий сокет. Родительский процесс закрывает присоединенный сокет. Затем дочерний процесс вызывает функцию str_echo для обработки запроса клиента. Она выполняет серверную обработку запроса клиента - считывание строк и отражение их клиенту .


 void str_echo(int sockfd)
 {
 	ssize_t		n;
 	char		line[MAXLINE];
 
 	for ( ; ; ) {
 		if ( (n = Readline(sockfd, line, MAXLINE)) == 0)
 			return;		/* connection closed by other end */
 
 		Writen(sockfd, line, n);
 	}
 }
 
Функция Readline читает строку из сокета , после чего строка отражается обратно клиенту с помощью функции Writen. Если клиент надумает закрыть соединение , функция Readline вернет ноль , после чего мы выходим из str_echo и завершаем дочерний процесс.

Клиент

Это файл tcpcli01.c - исполняемый файл будет называться client01

 #include	"unp.h"
 
 int main(int argc, char **argv)
 {
 	int					sockfd;
 	struct sockaddr_in	servaddr;
 
 	if (argc != 2)
 		err_quit("usage: tcpcli ");
 
 	sockfd = Socket(AF_INET, SOCK_STREAM, 0);
 
 	bzero(&servaddr, sizeof(servaddr));
 	servaddr.sin_family = AF_INET;
 	servaddr.sin_port = htons(SERV_PORT);
 	Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
 
 	Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
 
 	str_cli(stdin, sockfd);		/* do it all */
 
 	exit(0);
 }
При запуске клиента в командной строке нужно указать ip-адрес (для локального варианта - 127.0.0.1). Функция connect устанавливает соединение с сервером. Затем работает функция str_cli - считывает строку текста и отправляет ее серверу:

 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);
 	}
 }
 
После запуска набираем текст и жмем на enter. Затем текст уходит на сервер , отражается и самораспечатывается ниже 2-й раз. Выход из клиента - Ctrl-D

Порядок завершения работы :
1. Когда мы на клиенте набираем Ctrl-D ,мы выходим из str_cli
2. Клиентская функция main завершает работу
3. Клиентский сокет закрывается ядром.
4. Дочерний процесс сервера блокируется
5. TCP-соединение завершается
6. При завершении дочернего серверного процесса родителю отправляется сигнал SIGCHLD. Но мы его игнорируем , после чего дочерний процесс превращается в зомби , что ни есть хорошо.

Функция signal

Сигнал - уведомление процессу о том , что произошло некое событие. Иногда сигналы называют программными прерываниями (software interrupts). Сигналы могут посылаться в следующих направлениях :
1. одним процессом другому процессу
2. ядром процессу
Сигнал SIGCHLD посылается ядром процессу. Каждому сигналу соответствует действие - action. Оно задается с помощью вызова функции sigaction. Есть 3 способа задать такое действие :
1. Можно написать функцию , которая вызывается при перехвате сигнала - она называется обработчиком сигнала - signal handler, а действие называется перехватом сигнала - catching. Сигналы SIGKILL и SIGSTOP перехватывать нельзя. Наша функция вызывается с одним аргументом :
 	void handler(int signo);
 

2. Можно игнорировать сигнал , если действие задать как SIG_IGN.
3. Можно установить действие по умолчанию - SIG_DFL. Обычно это приводит к завершению процесса.

Определим функцию signal. 1-й аргумент - имя сигнала , второй - указатель на функцию :


 Sigfunc *
 signal(int signo, Sigfunc *func)
 {
 	struct sigaction	act, oact;
 
 	act.sa_handler = func;
 	sigemptyset(&act.sa_mask);
 	act.sa_flags = 0;
 	if (signo == SIGALRM) {
 #ifdef	SA_INTERRUPT
 		act.sa_flags |= SA_INTERRUPT;	/* SunOS 4.x */
 #endif
 	} else {
 #ifdef	SA_RESTART
 		act.sa_flags |= SA_RESTART;		/* SVR4, 44BSD */
 #endif
 	}
 	if (sigaction(signo, &act, &oact) < 0)
 		return(SIG_ERR);
 	return(oact.sa_handler);
 }
 /* end signal */
 
Обработчик - элемент sa_handler структуры sigaction - равен аргументу func функции signal.
Маска сигнала - sa_mask - равна null. Это означает , что во время работы этого обработчика другие сигналы не будут блокироваться.
Однажды установленный обработчик сигналов остается установленным каждый раз и после выполнения.
На время выполнения обработчика сигнал блокируется.
Если в период блокировки сигналы доставляются несколько раз , они теряются.
Выборочная блокировка сигналов возможна с помощью sigprocmask.

Обработка сигналов SIGCHLD

Если у завершающегося процесса есть дочерний процесс в зомбированном состоянии , идентификатору родительского процесса присваивается значение 1 , и это родительский процесс будет ждать завершения всех своих зомби.

Зомби вообще ни есть хорошо . Когда создается дочерний процесс с помощью fork , нужно с помощью функции wait дождаться их завершения . Для этого мы установим обработчик сигналов для сигнала SIGCHLD и внутри его вызовем функцию wait :


 	Signal(SIGCHLD,sig_chld);	
 
Обработчик сигнала - функция sig_chld :

 void
 sig_chld(int signo)
 {
 	pid_t	pid;
 	int		stat;
 	pid = wait(&stat);
 	printf("child %d terminated\n", pid);
 	return;
 }
 
Термином медленный системный вызов - slow system call - мы назовем любой системный вызов , который может быть заблокирован навсегда. Он может никогда не завершиться. В эту категорию попадают большинство сетевых функций. Например , нет никакой гарантии , что вызов функции accept будет когда-нибудь завершен , если нет клиентов , соединенных с сервером. То же самое касается серверной функции readline в том случае , если клиент никогда не пошлет строку.

Для обработки завершенного дочернего процесса мы вызываем функцию wait :


 	pid_t wait(int *statloc);
 	pid_t waitpid(pid_t pid, int *statloc, int options);
 
Обе функции возвращают 2 значения. Это идентификатор завершенного дочернего процесса и статус завершения дочернего процесса. Функция waitpid более гибкая . Аргумент pid задает идентификатор процесса , который мы будем ожидать.

Для уяснения разницы между wait и waitpid мы напишем нового клиента , который установит с сервером 5 соединений, а затем использует первое из этих соединений - sockfd[0] - в вызове функции str_cli.

Для этого сначала соберем новую версию сервера :
Это файл tcpserv03.c - и соответственно его бинарник server2


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

2-я версия клиента - файл tcpcli04.c - и бинарник client02 :


 #include	"unp.h"
 
 int
 main(int argc, char **argv)
 {
 	int					i, sockfd[5];
 	struct sockaddr_in	servaddr;
 
 	if (argc != 2)
 		err_quit("usage: tcpcli < IPaddress >");
 
 	for (i = 0; i < 5; i++) {
 		sockfd[i] = Socket(AF_INET, SOCK_STREAM, 0);
 
 		bzero(&servaddr, sizeof(servaddr));
 		servaddr.sin_family = AF_INET;
 		servaddr.sin_port = htons(SERV_PORT);
 		Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
 
 		Connect(sockfd[i], (SA *) &servaddr, sizeof(servaddr));
 	}
 
 	str_cli(stdin, sockfd[0]);		/* do it all */
 
 	exit(0);
 }
 
Когда этот клиент завершает работу, все открытые дескрипторы автоматом закрываются ядром, это делается в результате exit, мы даже не вызываем close, и все 5 соединений завершаются примерно одновременно. В модифицированном сервере теперь есть функция signal для установки обработчика сигнала SIGCHLD. Если после запуска мы будем набирать строку для посылки серверу , отображение получим только от одной . Здесь появляются 4 зомби. Для решения проблемы зомби вместо функции wait нужно использовать функцию waitpid - и следующая - 3-я - версия сервера :

Это файл tcpserv04.c - и соответственно бинарник server3 :


 #include	"unp.h"
 
 int
 main(int argc, char **argv)
 {
 	int					listenfd, connfd;
 	pid_t				childpid;
 	socklen_t			clilen;
 	struct sockaddr_in	cliaddr, servaddr;
 	void				sig_chld(int);
 
 	listenfd = Socket(AF_INET, SOCK_STREAM, 0);
 
 	bzero(&servaddr, sizeof(servaddr));
 	servaddr.sin_family      = AF_INET;
 	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
 	servaddr.sin_port        = htons(SERV_PORT);
 
 	Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
 
 	Listen(listenfd, LISTENQ);
 
 	Signal(SIGCHLD, sig_chld);	/* must call waitpid() */
 
 	for ( ; ; ) {
 		clilen = sizeof(cliaddr);
 		if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
 			if (errno == EINTR)
 				continue;		/* back to for() */
 			else
 				err_sys("accept error");
 		}
 
 		if ( (childpid = Fork()) == 0) {	/* child process */
 			Close(listenfd);	/* close listening socket */
 			str_echo(connfd);	/* process the request */
 			exit(0);
 		}
 		Close(connfd);			/* parent closes connected socket */
 	}
 }
 
В этом варианте сервера видоизмененная версия sig_chld

 void
 sig_chld(int signo)
 {
 	pid_t	pid;
 	int		stat;
 
 	while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
 		printf("child %d terminated\n", pid);
 	return;
 }
 
В результате - наконец-то - после завершения клиентского приложения - сервер выведет на экран . что все 5 зомби принудительно терминированы . А все потому , что waitpid тут вызывается в цикле. Необходимо задать параметр WHONAME - это не дает waitpid завершиться раньше времени.Мы не можем тот же фокус в цикле повторить с wait.

В исходниках вы можете найти версию сервера - server4 - которая принимает от клиента 2 числа в одной строке и возвращает сумму этих чисел.

Также в исходниках можно найти версию сервера - server5 - и клиента - client03 - где через сокет передаются 2 числа , но не в виде строки , а в виде двоичных данных.

Исходники лежат тут

Оставьте свой комментарий !

Ваше имя:
Комментарий:
Оба поля являются обязательными

 Автор  Комментарий к данной статье