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