Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
СПО / Semestr 2 / Lectures 2semestr / Lecture 2_01 / fork_threads_mutex_FIFO_sockets.doc
Скачиваний:
27
Добавлен:
11.04.2015
Размер:
301.57 Кб
Скачать

Семафоры

Это не совсем традиционное средство IPC, так как они не предназначены для передачи больших обьемов данных, а используется лишь для синхронизации доступа к разделяемым ресурсам. Вся их функциональность сводится к разрешению/запрещению доступа к разделяемому ресурсу.

Для создания/открытия группы семафоров необходимо получить идентификатор (с помощь все того же ftok()) и потом вызвать semget():

int semget(key_t key, int nsems, int semflgs);

key - идентификатор, nsems - количество семафоров в создаваемой группе (если мы открываем уже созданную группу, то этот параметр игнорируется), semflgs - права доступа, которые задаются абсолютно аналогично msgflg в вызове msgget(). Возвращается дескриптор созданной/открытой группы семафоров или -1 в случае ошибки. Получив дескриптор, можем оперировать состояниями семафора с помощью системного вызова semop():

int semop(int semid, struct sembuf *semop, size_t nops);

semid - полученный с помощью semget дескриптор, nops - количество операций в команде, и semop - по сути сам набор команд, который представляет собой массив структур из трех записей, где каждый элемент массива - команда, поданная структурой sembuf:

struct sembuf {

unsigned short sem_num; // номер семафора

short sem_op // операция

short sem_flg // флаги операции

}

Операция определяется следующими значениями sem_op:

  1. sem_op>0, то текущее значение семафора увеличивается на sem_op

  2. sem_op=0, то процесс будет ждать обнуления значения семафора

  3. sem_op<0, то процесс будет ждать, пока абсолютное значение семафора станет большим или равным, чем абсолютное значение sem_op и тогда абсолютная величина sem_op вычитается из значения семафора.

Чтобы понять все это рассмотрим один из примеров реализации бинарного семафора: есть некоторый ресурс, доступ к которому необходимо синхронизировать. Пусть занятость ресурса обозначается значением 1 семафора, а доступность - 0. Тогда набор из двух команд:

struct sembuf sem_wait_lock[2] = {

0, 0, 0, // ждем, когда семафор #0 обнулится

0, 1, 0 // установим его значение в 1

}

будет ожидать установки значения семафора с номером 0 в 0 (то есть освобождения ресурса), а потом заблокирует ресурс, установив его значение в 1. Команда:

struct sembuf sem_unlock[1] = {

0, -1, 0 // Обнулим семафор

}

будет разблокировать ресурс, если он занят.

Уничтожение группы семафоров делается с помощью semctl:

int semctl(int semid, int semnum, int cmd, ...);

Которая выполняет команду cmd над semnum'тым семафором в группе semid. cmd, равный IPC_RMID определяет операцию уничтожения группы семафоров с идентификатором semid.

Разделяемая память

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

По аналогии с другими средствами UNIX System V IPC работа с разделяемой памятью начинается с получения ключа с помощью ftok(), и системного вызова shmget(), который используется для создания/открытия разделяемой памяти:

int shmget(key_t key, int size, int shmflag);

Думаю, что объяснять, что такое key и shmflag уже не надо. :-). size же определяет размер выделяемой области памяти. Возвращается же либо идентификатор созданной области, либо -1. Но далее начинаются отличия. Сперва выделенную область необходимо "присоединить". За это отвечает вызов shmat():

char *shmat(int shmid, char *shmaddr, int shmflag);

тут тоже shmid - полученный через shmget идентификатор созданной области, а shmaddr и shmflag позволяют более тонко определить адрес для присоединения. Установкой shmaddr в 0 мы предоставляем системе самой выбрать адрес. Возвращается же адрес начала области, выделенной предшествующим вызовом shmget.

Перед уничтожением выделенной области разделяемой памяти необходимо от нее отключится с помощью

int shmdt(const void shmaddr);

где shmaddr - указатель на начало области выделенной памяти (то есть то, что возвращает shmat).

Опять же, по аналогии с сообщениями и семафорами за дополнительную работу с разделяемой памятью (в т. ч. и ее уничтожение) отвечает вызов (все параметры - по аналогии с уже рассмотренными):

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

Ух... А теперь наконец-то пример, чтобы закрепить эту скучную теорию. Тут мы снова рассмотрим пример сервера, а клиент вы напишете самостоятельно. Для удобства вынесем часть определений в заголовочный файл semshmex.h:

/* Структура, используемая для передачи

данных посредством разделяемой памяти */

typedef struct Msg_buf {

int seg;

int data1;

} Messg;

/* Комманды семафора для синхронизации

выполнения приложений */

static struct sembuf proc_wait[1] = {

1, -1, 0 }; /* Ждем когда значение семафора #1

станет 1 и обнулим его */

static struct sembuf proc_run[1] = {

1, 1, 0 }; // Установим значение семафора #1 в 1

/* Комманды семафора для блокирования памяти

sem_wait_lock и sem_unlock скопировать

из раздела про семафоры */

Ну и сам код. Я постараюсь его хорошо прокомментировать, чтобы не нужно было ничего объяснять после, ОК? :-)

#include <stdio.h>

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

#include <sys/shm.h>

#include "shmsrv.h"

int main() {

// Получаем ключ

key_t key=ftok("server", 'a');

// Выделяем разделяемую память

int shmid=shmget(key, sizeof(Messg), 0666 | IPC_CREAT);

// Присоединяем разделяемую память

Messg *mymsg=(Messg *)shmat(shmid, 0, 0);

/* Создаем группу из двух семафоров: одну - для

синхронизации выполнения программ, другую - для

синхронизации доступа к разделяемой памяти */

int semid=semget(key, 2, 0666 | IPC_CREAT);

// Ждем начала работы клиента

semop(semid, &proc_wait[0], 1);

/* Ждем пока клиент запишет данные в разделяемую

область памяти и потом заблокируем ее */

semop(semid, &sem_wait_lock[0], 2);

/* Все отлично. Теперь через mymsg можно

получить доступ к этой памяти */

printf("Got data: %d\n", mymsg->data1);

// Разблокируем разделяемую память

semop(semid, &sem_unlock[0], 1);

// Приберем за собой :-)

// Отсоединим разделяемую память

shmdt(mymsg);

// Освобождаем разделяемую память...

shmctl(shmid, IPC_RMID, 0);

// ...и группу семафоров

semctl(semid, IPC_RMID, 0);

exit(0);

}

Вот и все. Клиент на вашей совести. Я дам лишь общую схему работы:

  1. Получить ключ и доступ к разделяемой памяти и соответствующей группе семафоров (все по аналогии только флаги можно и опустить).

  2. Присоединить через shmat() выделенную область. Потом через команду sem_wait_lock заблокировать его и через proc_run сообщить серверу, что мы начали работу.

  3. Записать данные в mymsg->data1.

  4. Разблокировать память через sem_unlock и отсоединится от нее через shmdt().

Вот и все. И еще одно: как вы могли заметить, в примерах практически нет ни одной проверки корректности работы через возвращаемые системными вызовами значения. Я шел (и скорее всего буду идти в дальнейшем) на это сознательно дабы сделать код более компактным, но вообще говоря отсутствие такого контроля говорит о том, что уровень у программиста ниже плинтуса :-) . Потрудитесь выполнять проверки на предмет возвращения -1 там, где это возможно. Далее я это оговаривать не буду, но там где проверок нет, но можно поставить - обязательно ставьте - приучайтесь писать хорошие программы.

Вообще говоря, использование вышеописанных средств я встречал сравнительно редко (именно в исходниках программ), но знать о них полезно, поскольку в некоторых приложениях будет удобнее использовать именно их, а не что-либо другое. Более подробно о UNIX System V IPC можно прочитать в уже упоминавшейся мной книге "Операционная система UNIX" А. Робачевского, а также: в книге "UNIX: Взаимодействие процессов" У. Стивенса и Linux Programmers Guide (http://www.ibiblio.org/pub/Linux/docs/LDP/programmers-guide/).

Sockets

Ну и самое вкусное из IPC -- напоследок :-) . BSD-сокеты -- это наиболее распространенное и удобное средство взаимодействия процессов.

Чем же они так примечательны? Хотя бы тем, что сокеты могут быть использованы не только для обеспечения передачи данных между процессами, запущенными на одной машине, но и для процессов, функционирующих на разных (и даже территориально удаленных) компьютерах с абсолютно разными платформами (Windows/UNIX/MacOS и проч.). Кроме того, у них модульная организация, что позволяет с легкостью наращивать их функциональность за счет поддержки новых протоколов передачи данных.

И все же, что такое "сокет"? Наиболее наглядной, на мой взгляд, аналогией сокета является телефон. На уровне ОС же -- это некоторый абстрактный объект, коммуникационный узел, позволяющий передавать и принимать данные для породившего его процесса. Взаимодействие двух процессов с помощью сокетов заключается в том, что процессы создают сокеты (один процесс -- серверный, а другой -- клиентский), потом клиентский сокет инициирует подключение к серверному по его адресу (по аналогии с телефоном: "звонит, набирая его номер"), и если серверный отвечает ("снимает трубку"), то начинается обмен данными. Но не все так просто. Существует очень много разновидностей сокетов, которые могут отличаться адресным пространством и видом соединения, который лежит ниже уровня сокетов (хоть и не совсем полной, но все же аналогией является размещение телефонов в разных странах, а также импульсный/тоновый набор). Вид и свойства сокета определяется тройкой параметров: <коммуникационный домен, тип, протокол>.

Итак, первое: коммуникационный домен (communication domain) или семейство адресов (adress family). Он определяет в каком виде будет представлен адрес сокета а также семейство протоколов, которые можно использовать в рамках этого домена. Наиболее распространенными в UNIX являются:

  1. AF_UNIX: домен локального взаимодействия между процессами в пределах одной ОС UNIX. Адрес представляется в виде пути к файлу в файловой системе UNIX. Созданные в семействе адресов UNIX сокеты доступны только с локальной машины. Доступны только внутренние протоколы. Ради интереса, можете посмотреть свой каталог /tmp (дав команду ln -l и обратив внимание на первый символ параметров доступа -- если он равен s, то этот файл -- имя сокета). У меня, к примеру, там сейчас находятся файл xmms_serge.0, потому что запущен XMMS, который создал локальный сокет, используя который можно управлять ним из другой программы. Но этот файл не сам сокет, а всего лишь его имя.

  2. AF_INET: домен взаимодействия процессов удаленных систем. Адрес этого сокета представлен как интернет адрес DARPA, то есть парой: . Доступны протоколы Internet IPv4 (TCP/IP). Пример адреса: 192.168.0.1:80.

  3. AF_INET6: аналогично вышеуказанному, за исключением того, что используется IPv6 -- адресация.

Кстати, в мане на socket (man 2 socket) вам, возможно, повстречается определение макросов для доменов с префиксом PF_ (от Protocol Family), вместо AF_ (от Adress Family). Ничего страшного -- по сути это синонимы.

Второе: тип сокета. Он определяет "стиль соединения", используемый сокетом. Обратите также внимание на то, что в зависимости от выбранного домена вам могут быть доступны или не доступны те или иные типы сокетов. Ну а среди этих типов хотелось бы выделить следующие:

  1. SOCK_STREAM: это механизм "потоковой" (stream) передачи данных. Он надежен, обеспечивает двустороннюю последовательную передачу данных в полнодуплексном режиме (прием и отправка выполняются с одинаковой скоростью). Транспортный протокол (для AF_INET -- это TCP), который заведует передачей данных между сокетами этого типа контролирует в правильном ли порядке пришли данные, не было ли потерь данных, ошибок или дублирования. Для обмена данными по этому типу необходимо сначала произвести процедуру установки соединения, которое будет поддерживаться до момента разсоединения.

  2. SOCK_DGRAM: механизм, основанный на сообщениях (датаграммах). Datagram-сервисы ненадежны, поскольку независимые и небольшие сообщения просто отправляются транспортным протоколом (для AF_INET, например, -- это UDP) по указанному адресу и могут быть потерянны или продублированы. Но с другой стороны они обеспечивают высокоскоростную передачу данных, хоть и не гарантируют их доставку. При обмене данными по этому типу не осуществляется поддержка соединения.

  3. SOCK_RAW: обеспечивает доступ к более низкому уровню -- сетевому протоколу (для AF_INET таковым, например, является IP). С помощью этого типа можно, к примеру, контролировать данные, которые помещаются в заголовки пакетов IP. Этот тип сокета не может быть использован в домене AF_UNIX.

И, наконец, третье: протокол. Протокол -- это набор соглашений, которые регулируют обмен информацией между сторонами. Типичными протоколами являются TCP (IPPROTO_TCP), UDP (IPPROTO_UDP), ICMP (IPPROTO_ICMP), IP (IPPROTO_RAW). Набор возможных значений протоколов определяется выбранным коммуникационным доменом и типом сокета. То есть тройка не является приемлемой, поскольку потоковый тип передачи (stream) нельзя реализовать с помощью протокола UDP. Плюс ко всему иногда этот параметр можно опускать. Например, в домене AF_UNIX передача датаграмм задается сокетом вида: . То есть имя протокола опущено.

Итак, с абстракцией закончили. Теперь переходим к чему-то более земному. В сегодняшней статье мы рассмотрим только локальные сокеты (AF_UNIX), а через них откроется вид и на интернетовские. :) Для начала, как водится, попроще -- DGRAM-сокет, поскольку он не требует процедуры установки соединения. Программа в примере, если она запущенна в первый раз, будет создавать сокет, ожидать поступления данных с него и по мере поступления выводить их, а иначе -- предавать ему по сокетам строку, переданную ей в качестве аргумента (эту идею подсказало удобство использования download-менеджера X-Downloader по части добавления новых закачек :) ).

Первым делом нам необходимо создать сокет с помощью системного вызова socket():

int socket(int domain, int type, int protocol);

где domain, type, protocol -- рассмотренная выше тройка определяющая разновидность сокета. Возвращает она дескриптор ссылающийся на сокет или -1 если произошла ошибка. Но после этого созданный сокет еще нельзя использовать, т. к. он по сути только определяет его разновидность, но еще не имеет адреса. За операцию присвоения адреса, которое по терминологии называется связыванием (binding) сокета, отвечает вызов bind():

int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

где sockfd -- дескриптор нашего сокета, my_addr -- адрес с каким необходимо связать сокет и addrlen -- его длина. Но! Когда мы говорили о коммуникационных доменах, то упоминалось, что он будет и определять формат представления адреса. То есть структура sockaddr для доменов AF_UNIX, AF_UNET и других будет отличаться. Для нашего случая адрес сокета задается структурой sockaddr_un вида:

struct sockaddr_un {

sa_family_t sun_family; /* AF_UNIX */

char sun_path[108]; /* путь */

};

где sun_family -- адресное семейство (то есть коммуникационный домен), которое в этом случае всегда установлено в AF_UNIX, а sun_path -- имя файла, которое является именем сокета. Возвращается 0 в случае успеха и -1 с установкой errno при ошибке. Для деталей добро пожаловать в увлекательный мир man 7 unix :)

Так как сокеты датаграмм не требуют процедуры установки соединения, то мы сразу же можем приступить к приему/передаче данных. Делается это с помощью вызовов recvfrom() и sendto():

int recvfrom(int s, void *buf, size_t len, int flags,

struct sockaddr *from, socklen_t *fromlen);

Ну а тут s -- дескриптор сокета, buf -- буфер приема (место, куда запишутся принятые данные), len -- его размер, flags -- флаги, from -- указатель на структуру в которую запишется адрес передающей стороны (его можно использовать для ответа), fromlen -- сначала инициализируется размером буфера from, а при возврате функции содержит уже реальный размер адреса в from.

int sendto(int s, const void *msg, size_t len, int flags,

const struct sockaddr *to, socklen_t tolen);

Тут s -- опять же сокет, msg -- буфер, который содержит передаваемое сообщение, len -- его размер, to -- структура типа sockaddr_un, которая содержит адрес получателя и tolen -- его длинна.

Удаление сокета делается в два шага: удаления сокета в памяти и удаления файла (его имени) с диска. Первое делается с помощью вызова close():

int close(int fd);

где fd -- дескриптор сокета.

А второе -- с помощью unlink(), который как раз и используется для удаления имен и сопоставленных с ними файлов (помните, мы его и с FIFO-каналами использовали):

int unlink(const char *pathname);

передается, как вы уже поняли, имя (путь).

Ах да, еще вы нашем примере для создания уникального имени для клиентских сокетов мы будем использовать функцию mkstemp(), которая генерирует уникальное имя по шаблону:

char *mkstemp(char *template);

Шаблон (template) обязательно должен заканчиваться шестью символами XXXXXX. Замена именно этих символов делает из шаблона уникальную строку. Так как он будет модифицирован, то template обязательно должен быть массивом символов, а не символьной константой.

Ну вот добрались и до примера :) :

#include <stdio.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <sys/un.h>

#include <signal.h>

char buffer[256];

struct sockaddr_un sa; // Адрес серверного сокета

int ssock; // Дескриптор серверного сокета

/* Этот обработчик закрывает, а потом уничтожает

серверный сокет, когда серверный процесс прерывается

по комбинации Ctrl+C (Сигнал SIGINT) */

void siginth(int sig) {

close(ssock);

unlink(sa.sun_path);

exit(0);

}

int main(int argc, char** argv) {

/* Адрес клиента */

struct sockaddr_un ca;

int csock; // Сокет-клиент

int salen, calen; // Размеры адресов

// Создаем серверный сокет

if ((ssock = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) {

perror("socket");

exit(1);

}

// Определяем его адрес

bzero(&sa, sizeof(sa));

sa.sun_family = AF_UNIX;

strcpy(sa.sun_path, "/tmp/my_srvsocket");

salen = sizeof(sa.sun_family)+sizeof(sa.sun_path);

/* Пробуем привязать. Если вышло, то сервер еще не был

создан, а значит мы становимся серверным процессом */

if (bind(ssock, (struct sockaddr *) &sa, salen) >= 0) {

printf("Starting server...\nIncoming messages:\n");

signal(SIGINT, siginth);

for (;;) {

recvfrom(ssock, buffer, 256, 0, (struct sockaddr *) &ca, &calen);

printf("%s\n", buffer);

}

}

// Если не вышло, то сервер уже есть. Отправляем ему сообщение.

else {

/* "Убьем" дескриптор серверного сокета в

клиенте, т.к. он здесь не используется */

close(ssock);

// Сформируем адрес клиентского сокета

bzero(&ca, sizeof(sa));

ca.sun_family = AF_UNIX;

strcpy(ca.sun_path, "/tmp/my_clnt.XXXXXX");

mkstemp(ca.sun_path);

calen = sizeof(ca.sun_family)+sizeof(ca.sun_path);

// Создадим клиентский сокет и свяжем его с адресом

csock = socket(AF_UNIX, SOCK_DGRAM, 0);

bind(csock, (struct sockaddr *) &ca, calen);

// Отправим через него данные серверному

sendto(csock, argv[1], strlen(argv[1]), 0, (struct sockaddr *) &sa, salen);

// И уберем за собой

close(csock);

unlink(ca.sun_path);

}

exit(0);

}

Набираем, компиляем и проверяем (допустим, полученный исполняемый файл называется unsock_ex). В одном терминале набираем:

$ ./unsock_ex

и получаем запущенное "серверное" приложение. А в другом:

$ ./unsock_ex "Hello server. :)"

И смотрим на первый терминал. Там должна появится строчка "Hello server. :)". При запущенном сервере обратите внимание на содержимое каталога /tmp. Там должен находится файлик my_srvsocket. После прерывания серверного процесса по Ctrl+C он пропадает.

Вот и все на сегодня. Разбирайтесь, пишите... А в следующий раз мы поговорим о сокетах из семейства Internet адресов (то есть AF_INET). Хотя принципы работы и похожи, но имеются и небольшие, на которые и будет обращено ваше внимание, а также рассмотрен stream-механизм передачи данных. Удачи!

Как уже отмечалось в предыдущей статье, элемент из адресного семейства для домена INET задается не в виде файла, а в виде пары . Таким образом и структура подачи адреса будет отличаться от таковой для адресного семейства UNIX. Вот описание этой структуры:

struct sockaddr_in {

sa_family_t sin_family; /* адресное семейство: AF_INET */

u_int16_t sin_port; /* порт */

struct in_addr sin_addr; /* интернет адрес */

};

где sin_family -- название семейства адресов (он всегда установлен в AF_INET), sin_port, соответственно, порт к которому осуществляется доступ и sin_addr -- ip-адрес машины к которой мы будем подключаться. Этот адрес задается следующей структурой:

struct in_addr {

u_int32_t s_addr; /* адрес */

};

Обратите внимание, что здесь все целые числа используют так называемый сетевой порядок байт (network byte order). Но мы не будем заострять на этом внимание (ну есть такой стандарт и все тут :-) ), поскольку так или иначе все функции, которые используются для формирования адреса и порта возвращают данные именно в нем. Кроме того, существует набор функций которые позволяют производить конвертацию между порядками следования байт для хоста и сети. Вот, к примеру функции :

uint16_t htons(uint16_t hostshort);

uint32_t htonl(uint32_t hostlong);

Первая преобразует хостовый порядок байт (о чем свидетельствует первая буква "h" в названии функции) в сетевой (четвертая буква "n") для типа данных short ("s" в конце). Вторая делает аналогичное преобразование для long. А теперь угадайте как будут называться функции для обратного преобразования. :) Правильно, ntohs и ntohl.

Так, что нам еще понадобится? Ах да, функции для получения и преобразования адресов! Итак, gethostbyname(), gethostbyaddr(), inet_aton() и inet_ntoa().

Первая, gethostbyname(), имеет следующий синтаксис:

struct hostent *gethostbyname(const char *name);

Как видим, она принимает имя name в качестве параметра, который может представлять собой обычное каноническое DNS-имя типа "mycompany.com" или ip-адрес в форматах IPv4 (типа 192.169.0.1) или IPv6 (с разделителями в виде двоеточий или точек). Возвращает она структуру hostent:

struct hostent {

char *h_name; /* официальное имя хоста */

char **h_aliases; /* список синонимов */

int h_addrtype; /* тип адреса хоста */

int h_length; /* длинна адреса */

char **h_addr_list; /* список адресов */

}

С именами, синонимами и длинной, думаю, все понятно. h_addrtype всегда равен AF_INET, h_addr_list -- массив ip-адресов этого хоста в сетевом порядке байт.

Вторая, gethostbyaddr():

struct hostent *gethostbyaddr(const char *addr, int len, int type);

Возвращает указатель на аналогичную структуру, а параметрами принимает адрес (addr), его длину (len) и тип (type; хотя поддерживаемый тип пока только один -- AF_INET :-) ).

Третья, inet_aton() конвертирует ip-адрес поданный строкой в привычной нам форме в адрес с network byte order:

int inet_aton(const char *cp, struct in_addr *inp);

где cp -- строка с адресом, inp -- указатель на структуру, куда будет записан результат. Возвращает она не ноль, если адрес корректен и ноль иначе.

Четвертая, inet_ntoa():

char *inet_ntoa(struct in_addr in);

производит обратную конвертацию и по адресу in выдает строку с ip-адресом во все той же форме с точками-разделителями.

Итак, с адресацией немного разобрались. Теперь рассмотрим механизм потоковой (stream передачи данных). Как упоминалось в прошлой статье, этот механизм требует предварительной процедуры установки соединения. Честно говоря, я не хочу вдаваться в ее технические подробности (то есть того, как это происходит на более низком уровне с установкой битов в заголовках tcp-пакетов и т. д.) и отправлю вас на RTFM :) :

  1. Спецификация протокола TCP (RFC-793). Русский перевод здесь: http://www.mark-itt.ru/~alr/doc/doc_tcp/0005.ru.html

  2. Уильям Стивенс. "Протоколы TCP/IP. В подлиннике" издательства Вильямс на книжном рынке, или "TCP/IP крупным планом" на http://www.zeiss.net.ru/docs/technol/tcpip/tcp00.htm. Вообще говоря, это одна книга просто с двумя разными переводами.

  3. Говорят, что есть еще хорошая книга "Сети TCP/IP, том 1. Принципы, протоколы и структура" Дугласа Камера.

На себя же возьму ответственность показать как это все делается на программном уровне. Начнем с серверного процесса. Как и в случае с локальными сокетами UNIX, все начинается с его создания, посредством вызова socket() и его последующего связывания с определенным адресом. Но адрес в этом случае уже не локальный, а инетовский (как заполнять поля структуры sockaddr_in мы рассмотрим в примере). Далее необходимо оповестить систему, что он готов к приему запросов и выделить место под очередь запросов на установление связи. Для этого используется вызов listen():

int listen(int s, int backlog);

где s -- дескриптор созданного нами сокета, а backlog -- максимальный размер очереди запросов на установление соединения. Возвращается 0 в случае успеха и -1 с установкой errno -- в случае ошибки.

Все. Серверный сокет готов к работе. Но теперь нам необходимо "принимать входящие звонки" :-) , в смысле обрабатывать приходящие клиентские запросы. За это отвечает вызов accept():

int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

он извлекает первый запрос из очереди для сокета с дескриптором s и создает новый сокет со свойствами идентичными s и возвращает его дескриптор, который можно использовать для дальнейшей работы с клиентом (то есть обмена данных). При этом первый сокет остается нетронутым и может использоваться для приема следующих запросов из очереди, пока его дубликат "общается с клиентом". То есть получается система, похожая на работу секретаря с многоканальным телефоном :-) . Кроме всего прочего, accept() заполняет структуру адреса addr адресом клиента (обратите внимание, что вы должны предоставить указатель на соответствующую данному коммуникационному домену структуру, то есть, например, sockaddr_un для домена AF_UNIX), а addrlen -- это параметр типа значение-результат, который при вызове должен указывать на размер структуры addr, а после выполнения функции он будет иметь значение, равное реальному размеру возвращенного адреса. Типичный сценарий работы сервера по такому типу предполагает порождение дочернего процесса-обработчика для обмена данными с клиентом, пока основной процесс продолжает прослушивать последующие запросы. ,p>Так, а что же клиент? Ну, у него все еще проще. Во-первых, для него нет необходимости производить операцию связывания сокета bind(). Сразу после его создания необходимо лишь выполнить connect():

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

который инициирует соединение с другим сокетом, адрес которого подан структурой serv_addr (размером addrlen). sockfd -- дескриптор сокета-клиента, от имени которого мы соединяемся с сервером, и через который будем производить обмен данными. Возвращается классика: 0 или -1 с errno. :-)

Ну а далее и клиент и сервер приступаю к обмену данными. Делать это можно с помощью рассмотренных в предыдущей статье вызовов recvfrom() и sendto(). Или же просто recv():

ssize_t recv(int s, void *buf, size_t len, int flags);

и send():

ssize_t send(int s, const void *msg, size_t len, int flags);

которые, как вы видите, являются полными аналогами вышеупомянутых вызовов за исключением того, что здесь не передается адрес удаленного сокета. Но ведь это и не нужно, поскольку сокет с дескриптором s уже соединен с удаленным посредством connect()-accept().

Ну вот и до примера дошли. Для начала -- сервер.

#include <stdio.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <netdb.h>

#include <string.h>

/* Номер порта выбираем от фонаря :-) */

#define NPORT 1985

/* Функция отлова зомби :-) */

void zombiehunter(int sig) {

while (waitpid(-1, NULL, WNOHANG) > 0 ) ;

}

int main() {

int s, t, addrl;

struct sockaddr_in sa, ca;

/* готовим сервер */

/* заполняем поля локального адреса */

bzero(&sa, sizeof(struct sockaddr_in));

sa.sin_family = AF_INET;

sa.sin_addr.s_addr = INADDR_ANY;

/* Перед присвоением номера порта его необходимо

перевести в сетевой порядок байт */

sa.sin_port = NPORT;

sa.sin_port = htons(sa.sin_port);

// стандартная процедура создания и связывания

s = socket(AF_INET, SOCK_STREAM, 0);

bind(s, (struct sockaddr *) &sa, sizeof(struct sockaddr_in));

printf("Ready to answer querys on %s\n", inet_ntoa(sa.sin_addr));

/* Устанавливаем очередь размером 3 запроса */

listen(s, 3);

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

signal(SIGCHLD, zombiehunter);

for (;;) {

/* ждем клиентские запросы */

bzero(&ca, sizeof(ca));

addrl = sizeof(ca);

/* Принимаем запрос и дублируем сокет s в t для

дальнейшей обработки */

t = accept(s, (struct sockaddr *) &ca, &addrl);

printf("Got connection from %s\n", inet_ntoa(ca.sin_addr));

switch(fork()) {

case -1 : /* Эх... Неудача. */

perror("fork"); close(s); close(t);

exit(1); break;

case 0 : /* Мы в процессе-обработчике. За работу! */

/* В клиенте нам не нужен основной сокет */

close(s);

int rb; char buf[100];

/* Банальный echo-сервер... :-) */

while ((rb = recv(t, buf, sizeof(buf), 0)) != 0)

send(t, buf, rb, 0);

exit(0); break;

default : /* Мы в родителе */

/* В родителе нам не нужен сокет-дубликат */

close(t);

/* ждем следующее подключение */

continue;

}

}

close(s);

}

Итак, сразу бросается в глаза строчка:

sa.sin_addr.s_addr = INADDR_ANY;

Так вот, эта строка свидетельствует о том, что наш сервер "вешается" на все доступные интерфейсы. То есть если вы дадите команду:

$ netstat -na | grep LISTEN

то наш сервер будет представлен строкой вида:

tcp 0 0 0.0.0.0:1985 0.0.0.0:* LISTEN

где 0.0.0.0:1985 свидетельствует о том, что сервер прослушивает порт 1985 по всем доступным интерфейсам.

Еще одним моментом является обработка сигнала SIGCHLD. Он срабатывает в том случае, если дочерний процесс завершает свою работу. Таким образом пока идет обмен данными с клиентом и потомок работает ничего не происходит. Как только клиент отключаеися, обмен заканчивается и выполнение кода клиента доходит до exit(0) срабатывает сигнал SIGCHLD. В его обработчике мы ждем, когда дочерние процессы завершат свою работу, но так как он уже это сделал, то функция waitpid() завершается немедленно и освобождает занятые дочерним процессом системные ресурсы (то есть убивает зомби :-) ). В том же случае, если порожденных процесса два и один из них завершил работу, то для него waitpid() освобождает ресурсы, а завершения другого (рабочего) не ждет (то есть не приостанавливает работу основной программы до его завершения) за счет флага WNOHANG. Ради интереса можете посмотреть что показывает команда:

$ ps -auxfww

с обработкой сигнала и без нее.

Для проверки работы сервера достаточно его скомпилять, запустить и в другом терминале набрать

$ telnet localhost 1985

И набирать какие-нибудь сообщения. По идее вам должны возвращаться их точные копии.

А вот клиент по старой доброй традиции предстоит написать вам, а я как всегда только дам общие рекомендации. Итак, в клиенте нам нужно будет самим заполнять только адрес удаленного сокета. Причем его нужно будет достать с помощью gethostbyname(), например. В результате эта процедура будет иметь вид вроде:

struct hostent *remotehost = gethostbyname("127.0.0.1");

bzero(&sa, sizeof(struct sockaddr_in));

sa.sin_port = NPORT;

sa.sin_port = htons(sa.sin_port);

/* Возьмем первый попавшийся адрес из списка адресов h_addr_list */

bcopy(remotehost->h_addr_list[0], &sa.sin_addr, remotehost->h_length);

Потом создаем сокет и выполняем connect():

connect(s,(struct sockaddr *) &sa, sizeof(sa));

И переходим к процедуре обмена данными с помощью send() и recv(). Все по аналогии с сервером, только если вы пишете клиент для нашего сервера, то вам необходимо будет сначала отправить данные, а потом уже их принять.

Соседние файлы в папке Lecture 2_01