Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
 iakovlev.org 
 Books
  Краткое описание
 Linux
 W. R. Стивенс TCP 
 W. R. Стивенс IPC 
 A.Rubini-J.Corbet 
 K. Bauer 
 Gary V. Vaughan 
 Д Вилер 
 В. Сталлинг 
 Pramode C.E. 
 Steve Pate 
 William Gropp 
 K.A.Robbins 
 С Бекман 
 Р Стивенс 
 Ethereal 
 Cluster 
 Languages
 C
 Perl
 M.Pilgrim 
 А.Фролов 
 Mendel Cooper 
 М Перри 
 Kernel
 C.S. Rodriguez 
 Robert Love 
 Daniel Bovet 
 Д Джеф 
 Максвелл 
 G. Kroah-Hartman 
 B. Hansen 
NEWS
Последние статьи :
  Тренажёр 16.01   
  Эльбрус 05.12   
  Алгоритмы 12.04   
  Rust 07.11   
  Go 25.12   
  EXT4 10.11   
  FS benchmark 15.09   
  Сетунь 23.07   
  Trees 25.06   
  Apache 03.02   
 
TOP 20
 Linux Kernel 2.6...5170 
 Trees...939 
 Максвелл 3...870 
 Go Web ...823 
 William Gropp...803 
 Ethreal 3...787 
 Gary V.Vaughan-> Libtool...772 
 Ethreal 4...771 
 Rodriguez 6...763 
 Ext4 FS...755 
 Steve Pate 1...754 
 Clickhouse...753 
 Ethreal 1...742 
 Secure Programming for Li...731 
 C++ Patterns 3...716 
 Ulrich Drepper...696 
 Assembler...694 
 DevFS...661 
 Стивенс 9...649 
 MySQL & PosgreSQL...631 
 
  01.01.2024 : 3621733 посещений 

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 числа , но не в виде строки , а в виде двоичных данных.

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

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

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

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