Журнал LinuxFormat - перейти на главную

LXF83:Unix API

Материал из Linuxformat
(Различия между версиями)
Перейти к: навигация, поиск
(Парные сокеты)
м (восстановление кавычек в коде AWB)
 
(не показаны 6 промежуточных версий 3 участников)
Строка 1: Строка 1:
 +
{{Цикл/Unix API}}
 
== Сокеты ==
 
== Сокеты ==
 
'' '''ЧАСТЬ 3''' Сегодня ваши программы научатся общаться со внешним миром – следуйте за '''Андреем Боровским'''!''
 
'' '''ЧАСТЬ 3''' Сегодня ваши программы научатся общаться со внешним миром – следуйте за '''Андреем Боровским'''!''
Строка 10: Строка 11:
 
|-
 
|-
 
|}
 
|}
 +
 
В конкурсе на лучшую компьютерную идею всех времен и народов сокеты, без сомнения, могли бы рассчитывать на призовое место. Как и другие средства межпроцессного взаимодействия, рассмотренные в этой серии статей, сокеты впервые были реализованы именно на платформе Unix (4.2BSD), однако концепция сокетов как универсального средства обмена данными между процессами оказалась настолько удачна, что все современные системы поддерживают, по крайней мере, некоторое подмножество сокетов. Причины успеха сокетов заключаются в их простоте и универсальности. Программы, обменивающиеся данными с помощью сокетов, могут работать в одной системе и в разных, используя для обмена данными как специальные объекты системы, так и сетевой стек. Как и каналы, сокеты используют простой интерфейс, основанный на «файловых» функциях read(2) и write(2) (открывая сокет, программа Unix получает дескриптор файла, благодаря которому можно работать с сокетами, используя файловые функции), но, в отличие от каналов, сокеты позволяют передавать данные в обоих направлениях, как в синхронном, так и в асинхронном режиме. Большинство программистов используют для работы с сокетами различные библиотеки высокого уровня, однако высокоуровневые библиотеки, как правило, не позволяют задействовать всю мощь и многообразие сокетов. Наглядный пример многообразия – файловые сокеты. Программистам Windows должны быть знакомы сетевые сокеты, которые обычно организуют обмен данными с помощью протоколов семейства TCP/IP, однако в Unix есть и другие типы сокетов, специально предназначенные для обмена данными между локальными процессами.
 
В конкурсе на лучшую компьютерную идею всех времен и народов сокеты, без сомнения, могли бы рассчитывать на призовое место. Как и другие средства межпроцессного взаимодействия, рассмотренные в этой серии статей, сокеты впервые были реализованы именно на платформе Unix (4.2BSD), однако концепция сокетов как универсального средства обмена данными между процессами оказалась настолько удачна, что все современные системы поддерживают, по крайней мере, некоторое подмножество сокетов. Причины успеха сокетов заключаются в их простоте и универсальности. Программы, обменивающиеся данными с помощью сокетов, могут работать в одной системе и в разных, используя для обмена данными как специальные объекты системы, так и сетевой стек. Как и каналы, сокеты используют простой интерфейс, основанный на «файловых» функциях read(2) и write(2) (открывая сокет, программа Unix получает дескриптор файла, благодаря которому можно работать с сокетами, используя файловые функции), но, в отличие от каналов, сокеты позволяют передавать данные в обоих направлениях, как в синхронном, так и в асинхронном режиме. Большинство программистов используют для работы с сокетами различные библиотеки высокого уровня, однако высокоуровневые библиотеки, как правило, не позволяют задействовать всю мощь и многообразие сокетов. Наглядный пример многообразия – файловые сокеты. Программистам Windows должны быть знакомы сетевые сокеты, которые обычно организуют обмен данными с помощью протоколов семейства TCP/IP, однако в Unix есть и другие типы сокетов, специально предназначенные для обмена данными между локальными процессами.
  
 
=== Сокеты в файловом пространстве имен ===
 
=== Сокеты в файловом пространстве имен ===
 
Сокеты в файловом пространстве имен (file namespace, их еще называют «сокеты Unix») используют в качестве адресов имена файлов специального типа. Важной особенностью этих сокетов является то, что соединение с их помощью локального и удаленного приложений невозможно, даже если файловая система, в которой создан сокет, доступна удаленной операционной системе. В следующем фрагменте кода мы создаем сокет и связываем его с файлом socket.soc (полный текст программы-сервера fsserver.c можно найти на диске):
 
Сокеты в файловом пространстве имен (file namespace, их еще называют «сокеты Unix») используют в качестве адресов имена файлов специального типа. Важной особенностью этих сокетов является то, что соединение с их помощью локального и удаленного приложений невозможно, даже если файловая система, в которой создан сокет, доступна удаленной операционной системе. В следующем фрагменте кода мы создаем сокет и связываем его с файлом socket.soc (полный текст программы-сервера fsserver.c можно найти на диске):
<pre>
+
 
 +
<source lang="cpp">
 
sock = socket(AF_UNIX, SOCK_DGRAM, 0);
 
sock = socket(AF_UNIX, SOCK_DGRAM, 0);
 
if (sock < 0)
 
if (sock < 0)
 
{
 
{
   perror(“socket failed”);
+
   perror("socket failed");
 
   return EXIT_FAILURE;
 
   return EXIT_FAILURE;
 
}
 
}
 
srvr_name.sa_family = AF_UNIX;
 
srvr_name.sa_family = AF_UNIX;
strcpy(srvr_name.sa_data, “socket.soc”);
+
strcpy(srvr_name.sa_data, "socket.soc");
 
if (bind(sock, &srvr_name, strlen(srvr_name.sa_data) + sizeof(srvr_name.sa_family)) < 0)
 
if (bind(sock, &srvr_name, strlen(srvr_name.sa_data) + sizeof(srvr_name.sa_family)) < 0)
 
{
 
{
   perror(“bind failed”);
+
   perror("bind failed");
 
   return EXIT_FAILURE;
 
   return EXIT_FAILURE;
 
}
 
}
</pre>
+
</source>
  
 
Константы и функции, необходимые для работы с сокетами в файловом пространстве имен, объявлены в файлах sys/types.h и sys/socket.h. Как и файлы, сокеты в программах представлены дескрипторами. Дескриптор сокета можно получить с помощью функции socket(2). Первый параметр этой функции – домен, к которому принадлежит сокет. Домен сокета обозначает тип соединения (а не доменное имя Интернета, как вы могли бы подумать). Домен, обозначенный константой AF_UNIX, соответствует сокетам в файловом пространстве имен. Второй параметр, socket(), определяет тип сокета. Значение SOCK_DGRAM указывает датаграммный сокет (я предпочитаю этот вариант написания используемому в [1] «дейтаграммный»). Датаграммные сокеты осуществляют ненадежные соединения при передаче данных по сети и допускают широковещательную передачу данных. Другой часто используемый тип сокетов – SOCK_STREAM соответствует потоковым сокетам, реализующим соединения «точка-точка» с надежной передачей данных. Впрочем, в пространстве файловых имен датаграммные сокеты так же надежны, как и потоковые сокеты. Третий параметр функции socket() позволяет указать протокол, используемый для передачи данных. Мы оставляем значение этого параметра равным нулю. В случае ошибки функция socket() возвращает -1.
 
Константы и функции, необходимые для работы с сокетами в файловом пространстве имен, объявлены в файлах sys/types.h и sys/socket.h. Как и файлы, сокеты в программах представлены дескрипторами. Дескриптор сокета можно получить с помощью функции socket(2). Первый параметр этой функции – домен, к которому принадлежит сокет. Домен сокета обозначает тип соединения (а не доменное имя Интернета, как вы могли бы подумать). Домен, обозначенный константой AF_UNIX, соответствует сокетам в файловом пространстве имен. Второй параметр, socket(), определяет тип сокета. Значение SOCK_DGRAM указывает датаграммный сокет (я предпочитаю этот вариант написания используемому в [1] «дейтаграммный»). Датаграммные сокеты осуществляют ненадежные соединения при передаче данных по сети и допускают широковещательную передачу данных. Другой часто используемый тип сокетов – SOCK_STREAM соответствует потоковым сокетам, реализующим соединения «точка-точка» с надежной передачей данных. Впрочем, в пространстве файловых имен датаграммные сокеты так же надежны, как и потоковые сокеты. Третий параметр функции socket() позволяет указать протокол, используемый для передачи данных. Мы оставляем значение этого параметра равным нулю. В случае ошибки функция socket() возвращает -1.
Строка 40: Строка 43:
  
 
Если программа-сервер показалась вам простой, то программаклиент (fsclient.c) будет еще проще. Мы открываем сокет с помощью функции socket() и передаем данные (тестовую строку) серверу с помощью «напарника» recvfrom(), функции sendto(2):
 
Если программа-сервер показалась вам простой, то программаклиент (fsclient.c) будет еще проще. Мы открываем сокет с помощью функции socket() и передаем данные (тестовую строку) серверу с помощью «напарника» recvfrom(), функции sendto(2):
srvr_name.sa_family = AF_UNIX;
+
 
strcpy(srvr_name.sa_data, SOCK_NAME);
+
<source lang="cpp">
strcpy(buf, “Hello, Unix sockets!);
+
srvr_name.sa_family = AF_UNIX;
sendto(sock, buf, strlen(buf), 0, &srvr_name,
+
strcpy(srvr_name.sa_data, SOCK_NAME);
strlen(srvr_name.sa_data) + sizeof(srvr_name.sa_family));
+
strcpy(buf, "Hello, Unix sockets!");
 +
sendto(sock, buf, strlen(buf), 0, &srvr_name,
 +
strlen(srvr_name.sa_data) + sizeof(srvr_name.sa_family));
 +
</source>
  
 
Первый параметр функции sendto() – дескриптор сокета, второй и третий параметры позволяют указать адрес буфера для передачи данных и его длину. Четвертый параметр предназначен для передачи дополнительных флагов. Предпоследний и последний параметры несут информацию об адресе сервера и его длине, соответственно. Если при работе с датаграммными сокетами вызвать функцию connect(2) (см. ниже), то можно не указывать адрес назначения каждый раз (достаточно указать его один раз, как параметр функции connect()). Перед вызовом функции sendto() нам надо заполнить структуру sockaddr (переменную srvr_name) данными об адресе сервера. После окончания передачи данных мы закрываем сокет с помощью close(). Если вы запустите программу-сервер, а затем программу-клиент, то сервер распечатает тестовою строку, переданную клиентом.
 
Первый параметр функции sendto() – дескриптор сокета, второй и третий параметры позволяют указать адрес буфера для передачи данных и его длину. Четвертый параметр предназначен для передачи дополнительных флагов. Предпоследний и последний параметры несут информацию об адресе сервера и его длине, соответственно. Если при работе с датаграммными сокетами вызвать функцию connect(2) (см. ниже), то можно не указывать адрес назначения каждый раз (достаточно указать его один раз, как параметр функции connect()). Перед вызовом функции sendto() нам надо заполнить структуру sockaddr (переменную srvr_name) данными об адресе сервера. После окончания передачи данных мы закрываем сокет с помощью close(). Если вы запустите программу-сервер, а затем программу-клиент, то сервер распечатает тестовою строку, переданную клиентом.
Строка 51: Строка 57:
 
Сокеты в файловом пространстве имен похожи на именованные каналы тем, что для идентификации сокетов используются файлы специального типа. В мире сокетов есть и аналог неименованных каналов – парные сокеты (socket pairs). Как и неименованные каналы, парные сокеты создаются парами и не имеют имен. Естественно, что область применения парных сокетов та же, что и у неименованных каналов, – взаимодействие между родительским и дочерним процессом. Так же как и в случае неименованного канала, первый из дескрипторов используется одним процессом, второй – другим. В качестве примера использования парных сокетов мы рассмотрим программу sockpair.c, создающую два процесса с помощью fork(). Дочерние процессы sockpair.c используют парные сокеты для обмена вежливым английским приветствием.
 
Сокеты в файловом пространстве имен похожи на именованные каналы тем, что для идентификации сокетов используются файлы специального типа. В мире сокетов есть и аналог неименованных каналов – парные сокеты (socket pairs). Как и неименованные каналы, парные сокеты создаются парами и не имеют имен. Естественно, что область применения парных сокетов та же, что и у неименованных каналов, – взаимодействие между родительским и дочерним процессом. Так же как и в случае неименованного канала, первый из дескрипторов используется одним процессом, второй – другим. В качестве примера использования парных сокетов мы рассмотрим программу sockpair.c, создающую два процесса с помощью fork(). Дочерние процессы sockpair.c используют парные сокеты для обмена вежливым английским приветствием.
  
<pre>
+
<source lang="cpp">
 
#include <sys/types.h>
 
#include <sys/types.h>
 
#include <sys/socket.h>
 
#include <sys/socket.h>
Строка 57: Строка 63:
 
#include <stdio.h>
 
#include <stdio.h>
 
#include <errno.h>
 
#include <errno.h>
#define STR1 “How are you?
+
#define STR1 "How are you?"
#define STR2 “I’m ok, thank you.
+
#define STR2 "I'm ok, thank you."
 
#define BUF_SIZE 1024
 
#define BUF_SIZE 1024
 
int main(int argc, char ** argv)
 
int main(int argc, char ** argv)
Строка 67: Строка 73:
 
   if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) < 0)
 
   if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) < 0)
 
   {
 
   {
     perror(“socketpair() failed”);
+
     perror("socketpair() failed");
 
     return EXIT_FAILURE;
 
     return EXIT_FAILURE;
 
   }
 
   }
Строка 76: Строка 82:
 
     write(sockets[0], STR1, sizeof(STR1));
 
     write(sockets[0], STR1, sizeof(STR1));
 
     read(sockets[0], buf, sizeof(buf));
 
     read(sockets[0], buf, sizeof(buf));
     printf(%s\n”, buf);
+
     printf("%s\n", buf);
 
     close(sockets[0]);
 
     close(sockets[0]);
 
   }
 
   }
Строка 83: Строка 89:
 
     close(sockets[0]);
 
     close(sockets[0]);
 
     read(sockets[1], buf, sizeof(buf));
 
     read(sockets[1], buf, sizeof(buf));
     printf(%s\n”, buf);
+
     printf("%s\n", buf);
 
     write(sockets[1], STR2, sizeof(STR2));
 
     write(sockets[1], STR2, sizeof(STR2));
 
     close(sockets[1]);
 
     close(sockets[1]);
 
   }
 
   }
 
}
 
}
</pre>
+
</source>
  
 
Парные сокеты создаются функцией socketpair(2). У функции socketpair() четыре параметра. Первые три параметра функции те же, что и у socket(), а четвертым параметром является массив из двух переменных, в которых возвращаются дескрипторы. Дескрипторы сокетов, возвращенные socketpair(), уже готовы к передаче данных, так что мы сразу можем применять к ним функции read()/write(). После вызова fork() каждый процесс получает оба дескриптора, один из которых он должен закрыть. Для закрытия сокета мы используем функцию close().
 
Парные сокеты создаются функцией socketpair(2). У функции socketpair() четыре параметра. Первые три параметра функции те же, что и у socket(), а четвертым параметром является массив из двух переменных, в которых возвращаются дескрипторы. Дескрипторы сокетов, возвращенные socketpair(), уже готовы к передаче данных, так что мы сразу можем применять к ним функции read()/write(). После вызова fork() каждый процесс получает оба дескриптора, один из которых он должен закрыть. Для закрытия сокета мы используем функцию close().
Строка 95: Строка 101:
  
 
=== Сетевые сокеты ===
 
=== Сетевые сокеты ===
Мы переходим к рассмотрению самого важного и универсального
+
Мы переходим к рассмотрению самого важного и универсального типа сокетов – сетевых сокетов. Думаю, что о значении, которое имеют сетевые сокеты в Unix-системах, распространяться не нужно. Даже если вы пишете систему приложений, предназначенных для работы на одном компьютере, рассмотрите возможность использования сетевых сокетов для обмена данными между этими приложениями. Возможно, в будущем ваш программный комплекс наберет мощь и возникнет необходимость распределить его компоненты на нескольких машинах. Использование сетевых сокетов сделает процесс масштабирования проекта безболезненным. Впрочем, у сетевых сокетов есть и недостатки. Даже если сокеты используются для обмена данными на одной и той же машине, передаваемые данные должны пройти все уровни сетевого стека, что отрицательно сказывается на быстродействии и нагрузке на систему.
типа сокетов – сетевых сокетов. Думаю, что о значении, которое име-
+
 
ют сетевые сокеты в Unix-системах, распространяться не нужно. Даже
+
{{Врезка
если вы пишете систему приложений, предназначенных для работы на
+
|Заголовок=Остроконечники и тупоконечники
одном компьютере, рассмотрите возможность использования сетевых
+
|Содержание=Именно так переводятся на русский язык термины «little-endian» и «big-endian». В компьютерной литературе эти термины обозначают порядок байтов, используемый процессором для представления простых многобайтовых типов (например, 32-битного целого). В оригинале же (то есть в сказочной повести Дж. Свифта «Гулливер в стране лилипутов») так именовались враждебные общественные течения, приверженцы которых придерживались противоположных взглядов на порядок очистки яйца от скорлупы. Разногласия между остроконечниками и тупоконечниками даже стали причинной войны между Лилипутией и враждебным государством Блефуску.
сокетов для обмена данными между этими приложениями. Возможно,
+
 
в будущем ваш программный комплекс наберет мощь и возникнет
+
Впрочем, в компьютерном мире проблемы порядка байтов могут достигать совсем не лилипутских размеров. На процессорах Intel порядок байтов остроконечный, а, например, в системах MacOS X – Power PC Sun SPARC – тупоконечный (если учесть, что Apple отказалась от PowerPC, а Sun заменяет RISC-архитектуры на Opteron, получится, что остроконечники побеждают).
необходимость распределить его компоненты на нескольких машинах.
+
 
Использование сетевых сокетов сделает процесс масштабирования
+
Однако протоколы Интернета используют «тупоконечный» порядок байтов. Для того чтобы избежать путаницы, во всех системах, включая «тупоконечников» рекомендуется использовать функцию htons(). Эта функция «знает» порядок байтов в системе и, если нужно, приводит его в соответствие с принятым в протоколах TCP/IP. В русскоязычном Интернете одно время кочевала статья, в которой утверждалась (впрочем, шутливо), что чуждый для Intel порядок байтов и вызванная этим необходимость в дополнительной операции перестановки являются результатом заговора со стороны гигантских софтверных компаний. В книге [1] используются термины «прямой порядок байтов» для little-endian и «обратный порядок байтов» для big-endian.
проекта безболезненным. Впрочем, у сетевых сокетов есть и недостатки.
+
|Ширина=350px
Даже если сокеты используются для обмена данными на одной и той
+
}}
же машине, передаваемые данные должны пройти все уровни сетево-
+
 
го стека, что отрицательно сказывается на быстродействии и нагрузке
+
В качестве примера мы рассмотрим комплекс из двух приложений, клиента и сервера, использующих сетевые сокеты для обмена данными. Текст программы сервера вы найдете в файле netserver.c, ниже мы приводим некоторые фрагменты. Прежде всего, мы должны получить дескриптор сокета:
на систему.
+
 
В качестве примера мы рассмотрим комплекс из двух приложений,
+
<source lang="cpp">
клиента и сервера, использующих сетевые сокеты для обмена данными.
+
Текст программы сервера вы найдете в файле netserver.c, ниже мы
+
приводим некоторые фрагменты. Прежде всего, мы должны получить
+
дескриптор сокета:
+
 
sock = socket(AF_INET, SOCK_STREAM, 0);
 
sock = socket(AF_INET, SOCK_STREAM, 0);
 
if (socket < 0)
 
if (socket < 0)
 
{
 
{
printf(“socket() failed: %d\n”, errno);
+
  printf("socket() failed: %d\n", errno);
return EXIT_FAILURE;
+
  return EXIT_FAILURE;
 
}
 
}
В первом параметре функции socket() мы передаем константу
+
</source>
AF_INET, указывающую на то, что открываемый сокет должен быть
+
 
сетевым. Значение второго параметра требует, чтобы сокет был потоко-
+
В первом параметре функции socket() мы передаем константу AF_INET, указывающую на то, что открываемый сокет должен быть сетевым. Значение второго параметра требует, чтобы сокет был потоковым. Далее мы, как и в случае сокета в файловом пространстве имен, вызываем функцию bind():
вым. Далее мы, как и в случае сокета в файловом пространстве имен,
+
 
вызываем функцию bind():
+
<source lang="cpp">
 
serv_addr.sin_family = AF_INET;
 
serv_addr.sin_family = AF_INET;
 
serv_addr.sin_addr.s_addr = INADDR_ANY;
 
serv_addr.sin_addr.s_addr = INADDR_ANY;
Строка 130: Строка 132:
 
if (bind(sock, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
 
if (bind(sock, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
 
{
 
{
printf(“bind() failed: %d\n”, errno);
+
  printf("bind() failed: %d\n", errno);
return EXIT_FAILURE;
+
  return EXIT_FAILURE;
 
}
 
}
Переменная serv_addr, – это структура типа sockaddr_in. Тип
+
</source>
sockaddr_in специально предназначен для хранения адресов в фор-
+
 
мате Интернета. Самое главное отличие sockaddr_in от sockaddr_un –
+
Переменная serv_addr, – это структура типа sockaddr_in. Тип sockaddr_in специально предназначен для хранения адресов в формате Интернета. Самое главное отличие sockaddr_in от sockaddr_un – наличие параметра sin_port, предназначенного для хранения значения порта. Функция htons() переписывает двухбайтовое значение порта так, чтобы порядок байтов соответствовал принятому в Интернете (см. врезку). В качестве семейства адресов мы указываем AF_INET (семейство адресов Интернета), а в качестве самого адреса – специальную константу INADDR_ANY. Благодаря этой константе наша программа-сервер зарегистрируется на всех адресах той машины, на которой она выполняется.
наличие параметра sin_port, предназначенного для хранения значения
+
 
порта. Функция htons() переписывает двухбайтовое значение порта так,
+
Чтобы понять, что мы должны делать дальше, давайте вспомним, как работает сетевая подсистема Unix и, в данном случае, любой другой ОС. Сетевой сервер должен уметь выполнять запросы множества клиентов одновременно (наш сервер netserver.c фактически может обработать запрос только одного клиента, но речь сейчас идет об общем случае). При этом в соединениях «точка-точка», например, при использовании потоковых сокетов, у сервера для каждого клиента должен быть открыт отдельный сокет. Из этого следует, что мы не должны устанавливать соединение с клиентом через сам сокет sock, предназначенный для прослушивания входящих запросов (обычно, при использовании сетевых сокетов мы и не можем этого сделать), иначе все другие попытки соединиться с сервером по указанному адресу и порту будут заблокированы. Вместо этого мы вызываем функцию listen(2), которая переводит сервер в режим ожидания запроса на соединение:
чтобы порядок байтов соответствовал принятому в Интернете (см. врез-
+
 
ку). В качестве семейства адресов мы указываем AF_INET (семейство
+
<source lang="cpp">
адресов Интернета), а в качестве самого адреса – специальную конс-
+
танту INADDR_ANY. Благодаря этой константе наша программа-сер-
+
вер зарегистрируется на всех адресах той машины, на которой она
+
выполняется.
+
Чтобы понять, что мы должны делать дальше, давайте вспомним,
+
как работает сетевая подсистема Unix и, в данном случае, любой дру-
+
гой ОС. Сетевой сервер должен уметь выполнять запросы множества
+
клиентов одновременно (наш сервер netserver.c фактически может
+
обработать запрос только одного клиента, но речь сейчас идет об
+
общем случае). При этом в соединениях «точка-точка», например, при
+
использовании потоковых сокетов, у сервера для каждого клиента дол-
+
жен быть открыт отдельный сокет. Из этого следует, что мы не должны
+
устанавливать соединение с клиентом через сам сокет sock, предназна-
+
ченный для прослушивания входящих запросов (обычно, при использо-
+
вании сетевых сокетов мы и не можем этого сделать), иначе все другие
+
попытки соединиться с сервером по указанному адресу и порту будут
+
заблокированы. Вместо этого мы вызываем функцию listen(2), которая
+
переводит сервер в режим ожидания запроса на соединение:
+
 
listen(sock, 1);
 
listen(sock, 1);
Второй параметр listen() – максимальное число соединений, кото-
+
</source>
рые сервер может обрабатывать одновременно. Далее мы вызываем
+
 
функцию accept(2), которая устанавливает соединение в ответ на
+
Второй параметр listen() – максимальное число соединений, которые сервер может обрабатывать одновременно. Далее мы вызываем функцию accept(2), которая устанавливает соединение в ответ на запрос клиента:
запрос клиента:
+
 
 +
<source lang="cpp">
 
newsock = accept(sock, (struct sockaddr *) &cli_addr, &clen);
 
newsock = accept(sock, (struct sockaddr *) &cli_addr, &clen);
 
if (newsock < 0)
 
if (newsock < 0)
 
{
 
{
printf(“accept() failed: %d\n”, errno);
+
printf("accept() failed: %d\n", errno);
 
return EXIT_FAILURE;
 
return EXIT_FAILURE;
 
}
 
}
Получив запрос на соединение, функция accept() возвращает
+
</source>
новый сокет, открытый для обмена данными с клиентом, запро-
+
 
сившим соединение. Сервер как бы перенаправляет запрошенное
+
Получив запрос на соединение, функция accept() возвращает новый сокет, открытый для обмена данными с клиентом, запросившим соединение. Сервер как бы перенаправляет запрошенное соединение на другой сокет, оставляя сокет sock свободным для прослушивания запросов на установку соединения. Второй параметр функции accept() содержит сведения об адресе клиента, запросившего соединение, а третий параметр указывает размер второго. Так же, как и при вызове функции recvfom(), мы можем передать NULL в последнем и предпоследнем параметрах. Для чтения и записи данных сервер использует функции read() и write(), а для закрытия сокетов, естественно, close().
соединение на другой сокет, оставляя сокет sock свободным для
+
 
прослушивания запросов на установку соединения. Второй параметр
+
В программе-клиенте (netclient.c) нам, прежде всего, нужно решить задачу, с которой мы не сталкивались при написании сервера, а именно: выполнить преобразование доменного имени сервера в его сетевой адрес. Разрешение доменных имен выполняет функция gethostbyname():
функции accept() содержит сведения об адресе клиента, запросив-
+
 
шего соединение, а третий параметр указывает размер второго. Так
+
<source lang="cpp">
же, как и при вызове функции recvfom(), мы можем передать NULL в
+
последнем и предпоследнем параметрах. Для чтения и записи данных
+
сервер использует функции read() и write(), а для закрытия сокетов,
+
естественно, close().
+
В программе-клиенте (netclient.c) нам, прежде всего, нужно
+
решить задачу, с которой мы не сталкивались при написании серве-
+
ра, а именно: выполнить преобразование доменного имени сервера в
+
его сетевой адрес. Разрешение доменных имен выполняет функция
+
gethostbyname():
+
 
server = gethostbyname(argv[1]);
 
server = gethostbyname(argv[1]);
 
if (server == NULL)
 
if (server == NULL)
 
{
 
{
printf(“Host not found\n”);
+
  printf("Host not found\n");
return EXIT_FAILURE;
+
  return EXIT_FAILURE;
 
}
 
}
Функция получает указатель на строку с Интернет-именем сервера
+
</source>
(например, www.unix.com или 192.168.1.16) и возвращает указатель на
+
 
структуру hostent (переменная server), которая содержит имя сервера
+
Функция получает указатель на строку с Интернет-именем сервера (например, [http://www.unix.com www.unix.com] или 192.168.1.16) и возвращает указатель на структуру hostent (переменная server), которая содержит имя сервера в приемлемом для дальнейшего использования виде. При этом, если необходимо, выполняется разрешение доменного имени в сетевой адрес. Далее мы заполняем поля переменной serv_addr (структуры sockaddr_in) значениями адреса и порта:
в приемлемом для дальнейшего использования виде. При этом, если
+
 
необходимо, выполняется разрешение доменного имени в сетевой
+
<source lang="cpp">
адрес. Далее мы заполняем поля переменной serv_addr (структуры
+
sockaddr_in) значениями адреса и порта:
+
 
serv_addr.sin_family = AF_INET;
 
serv_addr.sin_family = AF_INET;
 
strncpy((char *)&serv_addr.sin_addr.s_addr,
 
strncpy((char *)&serv_addr.sin_addr.s_addr,
 
(char *)server->h_addr, server->h_length);
 
(char *)server->h_addr, server->h_length);
 
serv_addr.sin_port = htons(port);
 
serv_addr.sin_port = htons(port);
Программа-клиент открывает новый сокет с помощью вызова функ-
+
</source>
ции socket() аналогично тому, как это делает сервер (дескриптор соке-
+
 
та, который возвращает socket() мы сохраним в переменной sock), и
+
Программа-клиент открывает новый сокет с помощью вызова функции socket() аналогично тому, как это делает сервер (дескриптор сокета, который возвращает socket() мы сохраним в переменной sock), и вызывает функцию connect(2) для установки соединения:
вызывает функцию connect(2) для установки соединения:
+
 
 +
<source lang="cpp">
 
if (connect(sock, &serv_addr, sizeof(serv_addr)) < 0)
 
if (connect(sock, &serv_addr, sizeof(serv_addr)) < 0)
 
{
 
{
printf(“connect() failed: %d”, errno);
+
  printf("connect() failed: %d", errno);
return EXIT_FAILURE;
+
  return EXIT_FAILURE;
 
}
 
}
Теперь сокет готов к передаче и приему данных. Программа-клиент
+
</source>
считывает символы, вводимые пользователем в окне терминала. Когда
+
 
пользователь нажимает Enter, программа передает данные серверу,
+
Теперь сокет готов к передаче и приему данных. Программа-клиент считывает символы, вводимые пользователем в окне терминала. Когда пользователь нажимает Enter, программа передает данные серверу, ждет ответного сообщения сервера и распечатывает его.
ждет ответного сообщения сервера и распечатывает его.
+
 
На протяжении этой статьи мы несколько раз упоминали не-бло-
+
На протяжении этой статьи мы несколько раз упоминали не-блокирующие сокеты. Остановимся на них немного подробнее. О не-блокирующих сокетах вам нужно знать, прежде всего, то, что ими можно не пользоваться. Благодаря многопоточному программированию мы можем использовать блокирующие сокеты во всех ситуациях (и тогда, когда нам нужно обрабатывать несколько сокетов одновременно, и тогда, когда нам требуется возможность прервать операцию, выполняемую над сокетом). Рассмотрим, тем не менее, две функции, необходимые для работы с не-блокирующими сокетами. По умолчанию функция socket() создает блокирующий сокет. Чтобы сделать его не-блокирующим, мы используем функцию fcntl(2):
кирующие сокеты. Остановимся на них немного подробнее. О не-бло-
+
 
кирующих сокетах вам нужно знать, прежде всего, то, что ими можно
+
<source lang="cpp">
не пользоваться. Благодаря многопоточному программированию мы
+
можем использовать блокирующие сокеты во всех ситуациях (и тогда,
+
когда нам нужно обрабатывать несколько сокетов одновременно, и тог-
+
да, когда нам требуется возможность прервать операцию, выполняемую
+
над сокетом). Рассмотрим, тем не менее, две функции, необходимые
+
для работы с не-блокирующими сокетами. По умолчанию функция
+
socket() создает блокирующий сокет. Чтобы сделать его не-блокирую-
+
щим, мы используем функцию fcntl(2):
+
 
sock = socket(PF_INET, SOCK_STREAM, 0);
 
sock = socket(PF_INET, SOCK_STREAM, 0);
 
fcntl(sock, F_SETFL, O_NONBLOCK);
 
fcntl(sock, F_SETFL, O_NONBLOCK);
Теперь любой вызов функции read() для сокета sock будет возвра-
+
</source>
щать управление сразу же. Если на входе сокета нет данных для чте-
+
 
ния, функция read() вернет значение EAGAIN. Для поверки состояния
+
{{Врезка
не-блокирующих сокетов можно воспользоваться функцией select(2).
+
|Заголовок=Рекомендуемая литература:
Функция select() способна проверять состояние нескольких дескрип-
+
|Содержание=# Стивенс У., '''UNIX: Разработка сетевых приложений.''' – СПб.: Питер, 2004
торов сокетов (или файлов) сразу. Первый параметр функции – количе-
+
# W. R. Stevens, S. A. Rago, '''Advanced Programming in the UNIX® Environment: Second Edition''', Addison Wesley Professional, 2005
ство проверяемых дескрипторов. Второй, третий и четвертый параметры
+
|Ширина=350px
функции представляют собой наборы дескрипторов, которые следует
+
}}
проверять, соответственно, на готовность к чтению, записи и на наличие
+
 
исключительных ситуаций. Сама функция select() – блокирующая, она
+
Теперь любой вызов функции read() для сокета sock будет возвращать управление сразу же. Если на входе сокета нет данных для чтения, функция read() вернет значение EAGAIN. Для поверки состояния не-блокирующих сокетов можно воспользоваться функцией select(2). Функция select() способна проверять состояние нескольких дескрипторов сокетов (или файлов) сразу. Первый параметр функции – количество проверяемых дескрипторов. Второй, третий и четвертый параметры функции представляют собой наборы дескрипторов, которые следует проверять, соответственно, на готовность к чтению, записи и на наличие исключительных ситуаций. Сама функция select() – блокирующая, она возвращает управление, если хотя бы один из проверяемых сокетов готов к выполнению соответствующей операции. В качестве последнего параметра функции select() можно указать интервал времени, по прошествии которого она вернет управление в любом случае (тайм-аут). Вызов select() для проверки наличия входящих данных на сокете sock может выглядеть так:
возвращает управление, если хотя бы один из проверяемых сокетов
+
 
готов к выполнению соответствующей операции. В качестве последне-
+
<source lang="cpp">
го параметра функции select() можно указать интервал времени, по
+
прошествии которого она вернет управление в любом случае (тайм-аут).
+
Вызов select() для проверки наличия входящих данных на сокете sock
+
может выглядеть так:
+
 
fd_set set;
 
fd_set set;
 
struct timeval interval;
 
struct timeval interval;
Строка 253: Строка 216:
 
if (FD_ISSET(sock, &set)
 
if (FD_ISSET(sock, &set)
 
{
 
{
// Есть данные для чтения
+
  // Есть данные для чтения
 
}
 
}
Все, что касается функции select() теперь объявляется в заголо-
+
</source>
вочном файле sys/select.h (раньше объявления элементов функции
+
 
select() были разбросаны по файлам sys/types. h ,
+
Все, что касается функции select() теперь объявляется в заголовочном файле sys/select.h (раньше объявления элементов функции
sys/time.h и stdlib.h). В приведенном фраг-
+
select() были разбросаны по файлам sys/types.h , sys/time.h и stdlib.h). В приведенном фрагмента кода, FD_SET и FD_ISSET – макросы, предназначенные для работы с набором дескрипторов fd_set. На этом мы закончим знакомство с увлекательным миром межпроцессного взаимодействия Linux. Следующая статья будет посвящена управлению процессами, сигналам и потокам.
мента кода, FD_SET и FD_ISSET – мак-
+
 
росы, предназначенные для работы с
+
[[Категория:Учебники]]
набором дескрипторов fd_set.
+
На этом мы закончим зна-
+
комство с увлекательным миром
+
межпроцессного взаимодействия
+
Linux. Следующая статья будет
+
посвящена управлению процесса-
+
ми, сигналам и потокам.
+

Текущая версия на 16:54, 27 апреля 2008

Содержание

[править] Сокеты

ЧАСТЬ 3 Сегодня ваши программы научатся общаться со внешним миром – следуйте за Андреем Боровским!

«Сто лет тому назад дед нынешнего императора, в те времена еще наследный принц, за завтраком разбил яйцо с тупого конца и скорлупой порезал себе палец... С того времени все население Лилипутии разделилось на два лагеря – тупоконечников и остроконечников»
Дж. Свифт, «Гулливер в стране лилипутов»

В конкурсе на лучшую компьютерную идею всех времен и народов сокеты, без сомнения, могли бы рассчитывать на призовое место. Как и другие средства межпроцессного взаимодействия, рассмотренные в этой серии статей, сокеты впервые были реализованы именно на платформе Unix (4.2BSD), однако концепция сокетов как универсального средства обмена данными между процессами оказалась настолько удачна, что все современные системы поддерживают, по крайней мере, некоторое подмножество сокетов. Причины успеха сокетов заключаются в их простоте и универсальности. Программы, обменивающиеся данными с помощью сокетов, могут работать в одной системе и в разных, используя для обмена данными как специальные объекты системы, так и сетевой стек. Как и каналы, сокеты используют простой интерфейс, основанный на «файловых» функциях read(2) и write(2) (открывая сокет, программа Unix получает дескриптор файла, благодаря которому можно работать с сокетами, используя файловые функции), но, в отличие от каналов, сокеты позволяют передавать данные в обоих направлениях, как в синхронном, так и в асинхронном режиме. Большинство программистов используют для работы с сокетами различные библиотеки высокого уровня, однако высокоуровневые библиотеки, как правило, не позволяют задействовать всю мощь и многообразие сокетов. Наглядный пример многообразия – файловые сокеты. Программистам Windows должны быть знакомы сетевые сокеты, которые обычно организуют обмен данными с помощью протоколов семейства TCP/IP, однако в Unix есть и другие типы сокетов, специально предназначенные для обмена данными между локальными процессами.

[править] Сокеты в файловом пространстве имен

Сокеты в файловом пространстве имен (file namespace, их еще называют «сокеты Unix») используют в качестве адресов имена файлов специального типа. Важной особенностью этих сокетов является то, что соединение с их помощью локального и удаленного приложений невозможно, даже если файловая система, в которой создан сокет, доступна удаленной операционной системе. В следующем фрагменте кода мы создаем сокет и связываем его с файлом socket.soc (полный текст программы-сервера fsserver.c можно найти на диске):

sock = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sock < 0)
{
  perror("socket failed");
  return EXIT_FAILURE;
}
srvr_name.sa_family = AF_UNIX;
strcpy(srvr_name.sa_data, "socket.soc");
if (bind(sock, &srvr_name, strlen(srvr_name.sa_data) + sizeof(srvr_name.sa_family)) < 0)
{
  perror("bind failed");
  return EXIT_FAILURE;
}

Константы и функции, необходимые для работы с сокетами в файловом пространстве имен, объявлены в файлах sys/types.h и sys/socket.h. Как и файлы, сокеты в программах представлены дескрипторами. Дескриптор сокета можно получить с помощью функции socket(2). Первый параметр этой функции – домен, к которому принадлежит сокет. Домен сокета обозначает тип соединения (а не доменное имя Интернета, как вы могли бы подумать). Домен, обозначенный константой AF_UNIX, соответствует сокетам в файловом пространстве имен. Второй параметр, socket(), определяет тип сокета. Значение SOCK_DGRAM указывает датаграммный сокет (я предпочитаю этот вариант написания используемому в [1] «дейтаграммный»). Датаграммные сокеты осуществляют ненадежные соединения при передаче данных по сети и допускают широковещательную передачу данных. Другой часто используемый тип сокетов – SOCK_STREAM соответствует потоковым сокетам, реализующим соединения «точка-точка» с надежной передачей данных. Впрочем, в пространстве файловых имен датаграммные сокеты так же надежны, как и потоковые сокеты. Третий параметр функции socket() позволяет указать протокол, используемый для передачи данных. Мы оставляем значение этого параметра равным нулю. В случае ошибки функция socket() возвращает -1.

После получения дескриптора сокета мы вызываем функцию bind(2), которая связывает сокет с заданным адресом (связывать сокет с адресом необходимо в программе-сервере, но не в клиенте). Первым параметром функции является дескриптор, а вторым – указатель на структуру sockaddr (переменная srvr_name), содержащую адрес, на котором регистрируется сервер (третий параметр функции – длина структуры, содержащей адрес). Вместо общей структуры sockaddr для сокетов Unix (сокетов в файловом пространстве имен) можно использовать специализированную структуру sockaddr_un. Поле sockaddr. sa_family позволяет указать семейство адресов, которым мы будем пользоваться. В нашем случае это семейство адресов файловых сокетов Unix AF_UNIX. Сам адрес семейства AF_UNIX (поле sa_data) представляет собой обычное имя файла сокета. После вызова bind() наша программа-сервер становится доступна для соединения по заданному адресу (имени файла).

При обмене данными с датаграммными сокетами мы используем не функции write() и read(), а специальные функции recvfrom(2) и sendto(2). Эти же функции могут применяться и при работе с потоковыми сокетами, но в соответствующем примере мы воспользуемся «сладкой парочкой» read()/write(). Для чтения данных из датаграммного сокета мы используем функцию recvfrom(2), которая по умолчанию блокирует программу до тех пор, пока на входе не появятся новые данные.

bytes = recvfrom(sock, buf, sizeof(buf), 0, &rcvr_name, &namelen);

Вызывая функцию recvfrom(), мы передаем ей указатель на еще одну структуру типа sockaddr, в которой функция возвращает данные об адресе клиента, запросившего соединение (в случае файловых сокетов этот параметр не несет полезной информации). Последний параметр функции recvfrom() – указатель на переменную, в которой будет возвращена длина структуры с адресом. Если информация об адресе клиента нас не интересует, мы можем передать значения NULL в предпоследнем и последнем параметрах. По завершении работы с сокетом мы закрываем его с помощью «файловой» функции close(). Перед выходом из программы-сервера следует удалить файл сокета, созданный в результате вызова socket(), что мы и делаем с помощью функции unlink().

Если программа-сервер показалась вам простой, то программаклиент (fsclient.c) будет еще проще. Мы открываем сокет с помощью функции socket() и передаем данные (тестовую строку) серверу с помощью «напарника» recvfrom(), функции sendto(2):

srvr_name.sa_family = AF_UNIX;
strcpy(srvr_name.sa_data, SOCK_NAME);
strcpy(buf, "Hello, Unix sockets!");
sendto(sock, buf, strlen(buf), 0, &srvr_name,
strlen(srvr_name.sa_data) + sizeof(srvr_name.sa_family));

Первый параметр функции sendto() – дескриптор сокета, второй и третий параметры позволяют указать адрес буфера для передачи данных и его длину. Четвертый параметр предназначен для передачи дополнительных флагов. Предпоследний и последний параметры несут информацию об адресе сервера и его длине, соответственно. Если при работе с датаграммными сокетами вызвать функцию connect(2) (см. ниже), то можно не указывать адрес назначения каждый раз (достаточно указать его один раз, как параметр функции connect()). Перед вызовом функции sendto() нам надо заполнить структуру sockaddr (переменную srvr_name) данными об адресе сервера. После окончания передачи данных мы закрываем сокет с помощью close(). Если вы запустите программу-сервер, а затем программу-клиент, то сервер распечатает тестовою строку, переданную клиентом.

[править] Парные сокеты

Сокеты в файловом пространстве имен похожи на именованные каналы тем, что для идентификации сокетов используются файлы специального типа. В мире сокетов есть и аналог неименованных каналов – парные сокеты (socket pairs). Как и неименованные каналы, парные сокеты создаются парами и не имеют имен. Естественно, что область применения парных сокетов та же, что и у неименованных каналов, – взаимодействие между родительским и дочерним процессом. Так же как и в случае неименованного канала, первый из дескрипторов используется одним процессом, второй – другим. В качестве примера использования парных сокетов мы рассмотрим программу sockpair.c, создающую два процесса с помощью fork(). Дочерние процессы sockpair.c используют парные сокеты для обмена вежливым английским приветствием.

#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#define STR1 "How are you?"
#define STR2 "I'm ok, thank you."
#define BUF_SIZE 1024
int main(int argc, char ** argv)
{
  int sockets[2];
  char buf[BUF_SIZE];
  int pid;
  if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) < 0)
  {
    perror("socketpair() failed");
    return EXIT_FAILURE;
  }
  pid = fork();
  if (pid != 0)
  {
    close(sockets[1]);
    write(sockets[0], STR1, sizeof(STR1));
    read(sockets[0], buf, sizeof(buf));
    printf("%s\n", buf);
    close(sockets[0]);
  }
  else
  {
    close(sockets[0]);
    read(sockets[1], buf, sizeof(buf));
    printf("%s\n", buf);
    write(sockets[1], STR2, sizeof(STR2));
    close(sockets[1]);
  }
}

Парные сокеты создаются функцией socketpair(2). У функции socketpair() четыре параметра. Первые три параметра функции те же, что и у socket(), а четвертым параметром является массив из двух переменных, в которых возвращаются дескрипторы. Дескрипторы сокетов, возвращенные socketpair(), уже готовы к передаче данных, так что мы сразу можем применять к ним функции read()/write(). После вызова fork() каждый процесс получает оба дескриптора, один из которых он должен закрыть. Для закрытия сокета мы используем функцию close().

При взгляде на интерфейс программирования парных сокетов может возникнуть вопрос: а почему, собственно, эти функции относятся к сокетам? Ведь при работе с ними мы не используем ни адреса, ни модель «клиент-сервер». Это верно, но заметьте, что функции socketpair() передаются значения домена и типа сокета, так что и формально, и с точки зрения реализации в системе мы используем настоящие сокеты. Следует отметить, что указание домена в функции socketpair() выглядит явно излишне, поскольку для этой функции система поддерживает только сокеты в домене AF_UNIX (вполне логичное ограничение, если учесть, что парные сокеты не имеют имен и предназначены для обмена данными между родственными процессами).

[править] Сетевые сокеты

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


В качестве примера мы рассмотрим комплекс из двух приложений, клиента и сервера, использующих сетевые сокеты для обмена данными. Текст программы сервера вы найдете в файле netserver.c, ниже мы приводим некоторые фрагменты. Прежде всего, мы должны получить дескриптор сокета:

sock = socket(AF_INET, SOCK_STREAM, 0);
if (socket < 0)
{
  printf("socket() failed: %d\n", errno);
  return EXIT_FAILURE;
}

В первом параметре функции socket() мы передаем константу AF_INET, указывающую на то, что открываемый сокет должен быть сетевым. Значение второго параметра требует, чтобы сокет был потоковым. Далее мы, как и в случае сокета в файловом пространстве имен, вызываем функцию bind():

serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(port);
if (bind(sock, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
{
  printf("bind() failed: %d\n", errno);
  return EXIT_FAILURE;
}

Переменная serv_addr, – это структура типа sockaddr_in. Тип sockaddr_in специально предназначен для хранения адресов в формате Интернета. Самое главное отличие sockaddr_in от sockaddr_un – наличие параметра sin_port, предназначенного для хранения значения порта. Функция htons() переписывает двухбайтовое значение порта так, чтобы порядок байтов соответствовал принятому в Интернете (см. врезку). В качестве семейства адресов мы указываем AF_INET (семейство адресов Интернета), а в качестве самого адреса – специальную константу INADDR_ANY. Благодаря этой константе наша программа-сервер зарегистрируется на всех адресах той машины, на которой она выполняется.

Чтобы понять, что мы должны делать дальше, давайте вспомним, как работает сетевая подсистема Unix и, в данном случае, любой другой ОС. Сетевой сервер должен уметь выполнять запросы множества клиентов одновременно (наш сервер netserver.c фактически может обработать запрос только одного клиента, но речь сейчас идет об общем случае). При этом в соединениях «точка-точка», например, при использовании потоковых сокетов, у сервера для каждого клиента должен быть открыт отдельный сокет. Из этого следует, что мы не должны устанавливать соединение с клиентом через сам сокет sock, предназначенный для прослушивания входящих запросов (обычно, при использовании сетевых сокетов мы и не можем этого сделать), иначе все другие попытки соединиться с сервером по указанному адресу и порту будут заблокированы. Вместо этого мы вызываем функцию listen(2), которая переводит сервер в режим ожидания запроса на соединение:

listen(sock, 1);

Второй параметр listen() – максимальное число соединений, которые сервер может обрабатывать одновременно. Далее мы вызываем функцию accept(2), которая устанавливает соединение в ответ на запрос клиента:

newsock = accept(sock, (struct sockaddr *) &cli_addr, &clen);
if (newsock < 0)
{
printf("accept() failed: %d\n", errno);
return EXIT_FAILURE;
}

Получив запрос на соединение, функция accept() возвращает новый сокет, открытый для обмена данными с клиентом, запросившим соединение. Сервер как бы перенаправляет запрошенное соединение на другой сокет, оставляя сокет sock свободным для прослушивания запросов на установку соединения. Второй параметр функции accept() содержит сведения об адресе клиента, запросившего соединение, а третий параметр указывает размер второго. Так же, как и при вызове функции recvfom(), мы можем передать NULL в последнем и предпоследнем параметрах. Для чтения и записи данных сервер использует функции read() и write(), а для закрытия сокетов, естественно, close().

В программе-клиенте (netclient.c) нам, прежде всего, нужно решить задачу, с которой мы не сталкивались при написании сервера, а именно: выполнить преобразование доменного имени сервера в его сетевой адрес. Разрешение доменных имен выполняет функция gethostbyname():

server = gethostbyname(argv[1]);
if (server == NULL)
{
  printf("Host not found\n");
  return EXIT_FAILURE;
}

Функция получает указатель на строку с Интернет-именем сервера (например, www.unix.com или 192.168.1.16) и возвращает указатель на структуру hostent (переменная server), которая содержит имя сервера в приемлемом для дальнейшего использования виде. При этом, если необходимо, выполняется разрешение доменного имени в сетевой адрес. Далее мы заполняем поля переменной serv_addr (структуры sockaddr_in) значениями адреса и порта:

serv_addr.sin_family = AF_INET;
strncpy((char *)&serv_addr.sin_addr.s_addr,
(char *)server->h_addr, server->h_length);
serv_addr.sin_port = htons(port);

Программа-клиент открывает новый сокет с помощью вызова функции socket() аналогично тому, как это делает сервер (дескриптор сокета, который возвращает socket() мы сохраним в переменной sock), и вызывает функцию connect(2) для установки соединения:

if (connect(sock, &serv_addr, sizeof(serv_addr)) < 0)
{
  printf("connect() failed: %d", errno);
  return EXIT_FAILURE;
}

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

На протяжении этой статьи мы несколько раз упоминали не-блокирующие сокеты. Остановимся на них немного подробнее. О не-блокирующих сокетах вам нужно знать, прежде всего, то, что ими можно не пользоваться. Благодаря многопоточному программированию мы можем использовать блокирующие сокеты во всех ситуациях (и тогда, когда нам нужно обрабатывать несколько сокетов одновременно, и тогда, когда нам требуется возможность прервать операцию, выполняемую над сокетом). Рассмотрим, тем не менее, две функции, необходимые для работы с не-блокирующими сокетами. По умолчанию функция socket() создает блокирующий сокет. Чтобы сделать его не-блокирующим, мы используем функцию fcntl(2):

sock = socket(PF_INET, SOCK_STREAM, 0);
fcntl(sock, F_SETFL, O_NONBLOCK);


Теперь любой вызов функции read() для сокета sock будет возвращать управление сразу же. Если на входе сокета нет данных для чтения, функция read() вернет значение EAGAIN. Для поверки состояния не-блокирующих сокетов можно воспользоваться функцией select(2). Функция select() способна проверять состояние нескольких дескрипторов сокетов (или файлов) сразу. Первый параметр функции – количество проверяемых дескрипторов. Второй, третий и четвертый параметры функции представляют собой наборы дескрипторов, которые следует проверять, соответственно, на готовность к чтению, записи и на наличие исключительных ситуаций. Сама функция select() – блокирующая, она возвращает управление, если хотя бы один из проверяемых сокетов готов к выполнению соответствующей операции. В качестве последнего параметра функции select() можно указать интервал времени, по прошествии которого она вернет управление в любом случае (тайм-аут). Вызов select() для проверки наличия входящих данных на сокете sock может выглядеть так:

fd_set set;
struct timeval interval;
FD_SET(sock, &set);
tv.tv_sec = 1;
tv.tv_usec = 500000;
...
select(1, &set, NULL, NULL, &tv);
if (FD_ISSET(sock, &set)
{
  // Есть данные для чтения
}

Все, что касается функции select() теперь объявляется в заголовочном файле sys/select.h (раньше объявления элементов функции select() были разбросаны по файлам sys/types.h , sys/time.h и stdlib.h). В приведенном фрагмента кода, FD_SET и FD_ISSET – макросы, предназначенные для работы с набором дескрипторов fd_set. На этом мы закончим знакомство с увлекательным миром межпроцессного взаимодействия Linux. Следующая статья будет посвящена управлению процессами, сигналам и потокам.

Персональные инструменты
купить
подписаться
Яндекс.Метрика