Текст книги "Основы программирования в Linux"
Автор книги: Нейл Мэтью
Соавторы: Ричард Стоунс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 56 (всего у книги 67 страниц)
Если запустить приведенные версии серверной и клиентской программ на машине на базе процессора Intel под управлением Linux, то с помощью команды netstat
можно увидеть сетевые соединения. Эта команда есть в большинство систем UNIX, настроенных на работу в сети. Она отображает клиент-серверное соединение, ожидающее закрытия. Соединение закрывается после небольшой задержки. (Повторяем, что вывод в разных версиях Linux может отличаться.)
$ ./server2 & ./client2
[3] 23770
server waiting
server waiting
char from server = В
$ netstat -A inet
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address (State) User
tcp 1 0 localhost:1574 localhost:1174 TIME_WAIT root
Примечание
Прежде чем испытывать последующие примеры этой главы, убедитесь в том, что завершено выполнение серверных программ-примеров, поскольку они будут конкурировать при приеме соединений клиентов, и вы увидите вводящие в заблуждение результаты. Удалить их все (включая те, что будут приведены позже в этой главе) можно с помощью следующей команды:
killall server1 server2 server3 server4 server5
Вы сможете увидеть номера портов, присвоенные соединению сервера с клиентом. Локальный адрес отображает сервер, а внешний адрес – удаленного клиента. (Даже если клиент размещен на той же машине, он все равно подключается через сеть.) Для четкого разделения всех сокетов порты клиентов обычно отличаются от сокета сервера, ожидающего запросы на соединения, и уникальны в пределах компьютера.
Отображается локальный адрес (сокет сервера) 1574 (или может выводиться имя сервиса mvel-lm
) и выбранный в примере порт 9734. Почему они отличаются? Дело в том, что номера портов и адреса передаются через интерфейсы сокета как двоичные числа. В разных компьютерах применяется различный порядок байтов для представления целых чисел. Например, процессор Intel хранит 32-разрядное целое в виде четырех последовательных байтов памяти в следующем порядке 1-2-3-4, где 1-й байт – самый старший. Процессоры IBM PowerPC будут хранить целое со следующим порядком следования байтов: 4-3-2-1. Если используемую для хранения целых память просто побайтно копировать, два компьютера не придут к согласию относительно целочисленных значений.
Для того чтобы компьютеры разных типов могли согласовать значения многобайтовых целых чисел, передаваемых по сети, необходимо определить сетевой порядок передачи байтов. Перед передачей данных клиентские и серверные программы должны преобразовать собственное внутреннее представление целых чисел в соответствии с принятым в сети порядком следования байтов. Делается это с помощью функций, определенных в заголовочном файле netinet/in.h. К ним относятся следующие:
#include
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);
Эти функции преобразуют 16– и 32-разрядные целые из внутреннего формата в сетевой порядок следования байтов и обратно. Их имена соответствуют сокращенному названию выполняемых преобразований, например "host to network, long" (htonl, компьютерный в сетевой, длинные целые) и "host to network, short" (htons, компьютерный в сетевой, короткие целые). Компьютерам, у которых порядок следования байтов соответствует сетевому, эти функции предоставляют пустые операции.
Для обеспечения корректного порядка следования при передаче 16-разрядного целого числа ваши сервер и клиент должны применить эти функции к адресу порта. В программу server3.c следует внести следующие изменения:
server_address.sin_addr_s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(9734);
Результат, возвращаемый функцией inet_addr(«127.0.0.1»)
, преобразовывать не нужно, потому что в соответствии со своим определением она возвращает результат с сетевым порядком следования байтов. В программу client3.c необходимо внести следующее изменение:
address.sin_port = htons(9734);
В сервер, благодаря применению константы INADDR_ANY
, внесено изменение, позволяющее принимать запросы на соединение от любых IP-адресов.
Теперь, выполнив программы server3 и client3, вы увидите корректный номер порта, используемый для локального соединения:
$ netstat
Active Internet connections
Proto Recv-Q Send-Q Local Address Foreign Address (State) User
tcp 1 0 localhost:9734 localhost:1175 TIME_WAIT root
Примечание
Если вы пользуетесь компьютером, у которого собственный формат представления целых совпадает с сетевым порядком следования байтов, вы не увидите никакой разницы. Но для обеспечения корректного взаимодействия клиентов и серверов с разной архитектурой важно всегда применять функции преобразования.
Сетевая информация
До сих пор у клиентских и серверных программ были адреса и номера портов, компилируемые в них. В более универсальных серверных и клиентских программах для определения применяемых адресов и портов вы можете использовать данные сети.
Если у вас есть на это право, можно добавить свой сервер к списку известных сервисов в файл /etc/services, который назначает имена номерам портов, так что клиенты могут использовать вместо номеров символические имена сервисов.
Точно так же зная имя компьютера, можно определить IP-адрес, вызвав функции базы данных сетевых узлов (host database), которые найдут эти адреса. Делают они это, обращаясь за справкой к конфигурационным файлам, например, etc/hosts или к сетевым информационным сервисам, таким как NIS (Network Information Services (сервисы сетевой информации), ранее известным как Yellow Pages (желтые страницы)) и DNS (Domain Name Service, служба доменных имен).
Функции базы данных сетевых узлов или хостов (Host database) объявлены в заголовочном файле интерфейса netdb.h:
#include
struct hostent *gethostbyaddr(const void* addr, size_t len, int type);
struct hostent* gethostbyname(const char* name);
Структура, возвращаемая этими функциями, должна как минимум содержать следующие элементы.
struct hostent {
char *h_name; /* Имя узла */
char **h_aliases; /* Перечень псевдонимов (nicknames) */
int h_addrtype; /* Тип адреса */
int h_length; /* Длина адреса в байтах */
char **h_addr_list /* Перечень адреса (сетевой порядок байтов) */
};
Если в базе данных нет элемента, соответствующего заданному узлу или адресу, информационные функции вернут пустой указатель.
Аналогично информацию, касающуюся сервисов и связанных номеров портов, можно получить с помощью информационных функций сервисов:
#include
struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);
Параметр proto
задает протокол, который будет применяться для подключения к сервису, либо «tcp» для TCP-соединений типа SOCK_STREAM
, либо «udp» для UDP-дейтаграмм типа SOCK_DGRAM
.
Структура servent
содержит как минимум следующие элементы:
struct servent {
char *s_name; /* Имя сервиса */
char **s_aliases; /* Список псевдонимов (дополнительных имен) */
int s_port; /* Номер IP-порта */
char *s_proto; /* Тип сервиса, обычно «tcp» или «udp» */
}
Вы можете собрать воедино информацию о компьютере из базы данных сетевых узлов, вызвав функцию gethostbyname
и выведя ее результаты. Учтите, что адрес необходимо преобразовать в соответствующий тип и перейти от сетевого упорядочивания к пригодной для вывода строке с помощью преобразования inet_ntoa
, определенного следующим образом:
#include
char *inet_ntoa(struct in_addr in);
Функция преобразует адрес интернет-узла в строку формата четверки чисел с точками. В случае ошибки она возвращает -1, но в стандарте POSIX не определены конкретные ошибки. Еще одна новая функция, которую вы примените, – gethostname
:
#include
int gethostname(char *name, int name length);
Эта функция записывает имя текущего узла в строку, заданную параметром name
. Имя узла будет нуль-терминированной строкой. Аргумент namelength
содержит длину строкового имени и, если возвращаемое имя узла превысит эту длину, оно будет обрезано. Функция gethostname
возвращает 0 в случае успешного завершения и -1 в случае ошибки. И снова ошибки в стандарте POSIX не определены.
Выполните упражнение 15.5.
Упражнение 15.5. Сетевая информация
Данная программа getname.c получает сведения о компьютере.
1. Как обычно, вставьте соответствующие заголовочные файлы и объявите переменные:
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[]) {
char *host, **names, **addrs;
struct hostent *hostinfo;
2. Присвойте переменной host
значение аргумента, предоставляемого при вызове программы getname
, или по умолчанию имя машины пользователя:
if (argc == 1) {
char myname[256];
gethostname(myname, 255);
host = myname;
} else host = argv[1];
3. Вызовите функцию gethostbyname и сообщите об ошибке, если никакая информация не найдена:
hostinfo = gethostbyname(host);
if (!hostinfo) {
fprintf(stderr, «cannot get info for host: %sn», host);
exit(1);
}
4. Отобразите имя узла и любые псевдонимы, которые у него могут быть:
printf(«results for host %s:n», host);
printf(«Name : %sn», hostinfo->h_name);
printf("Aliases: ");
names = hostinfo->h_aliases;
while (*names) {
printf(« %s», *names); names++;
}
printf(«n»);
5. Если запрашиваемый узел не является IP-узлом, сообщите об этом и завершите выполнение:
if (hostinfo->h_addrtype != AF_INET) {
fprintf(stderr, «not an IP host!n»);
exit(1);
}
6. В противном случае выведите IP-адрес (адреса):
addrs = hostinfo->h_addr_list;
while (*addrs) {
printf(« %s», inet_ntoa(*(struct in_addr*)*addrs));
addrs++;
}
printf(«n»);
exit(0);
}
Для определения узла по заданному IP-адресу можно применить функцию gethostbyaddr
. Вы можете использовать ее на сервере для того, чтобы выяснить, откуда клиент запрашивает соединение.
Как это работает
Программа getname вызывает функцию gethostbyname для извлечения сведений об узле из базы данных сетевых узлов. Она выводит имя компьютера, его псевдонимы (другие имена, под которыми известен компьютер) и IP-адреса, которые он использует в своих сетевых интерфейсах. На одной из машин авторов выполнение примера и указание в качестве аргумента имени tilde привело к выводу двух интерфейсов: сети Ethernet и модемной линии связи.
$ ./getname tilde
results for host tilde:
Name: tilde.localnet
Aliases: tilde
192.168.1.1 158.152.x.x
Когда используется имя узла localhost
, задается виртуальная сеть:
$ ./getname localhost
results for host localhost:
Name: localhost
Aliases: 127.0.0.1
Теперь вы можете изменить свою программу-клиента для соединения с любым именованным узлом сети. Вместо подключения к серверу из вашего примера, вы соединитесь со стандартным сервисом и сможете извлечь номер порта.
Большинство систем UNIX и некоторые ОС Linux делают доступными свои системные время и дату в виде стандартного сервиса с именем daytime
. Клиенты могут подключаться к этому сервису для выяснения мнения сервера о текущих времени и дате. В упражнении 15:6 приведена программа-клиент getdate.c, именно это и делающая.
Упражнение 15.6. Подключение к стандартному сервису
1. Начните с обычных директив #include
и объявлений:
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[]) {
char *host;
int sockfd;
int len, result;
struct sockaddr_in address;
struct hostent *hostinfo;
struct servent *servinfo;
char buffer[128];
if (argc == 1) host = «localhost»;
else host = argv[1];
2. Найдите адрес узла и сообщите об ошибке, если адрес не найден:
hostinfo = gethostbyname(host);
if (!host info) {
fprintf(stderr, «no host: %sn», host);
exit(1);
}
3. Убедитесь, что на компьютере есть сервис daytime
:
servinfo = getservbyname(«daytime», «tcp»);
if (!servinfo) {
fprintf(stderr, «no daytime servicen»);
exit(1);
}
printf(«daytime port is %dn», ntohs(servinfo->s_port));
4. Создайте сокет:
sockfd = socket(AF_INET, SOCK_STREAM, 0);
5. Сформируйте адрес для соединения:
address.sin_family = AF_INET;
address.sin_port = servinfo->s_port;
address.sin_addr = *(struct in_addr *)*hostinfo->h_addr_list;
len = sizeof(address);
6. Затем подключитесь и получите информацию:
result = connect(sockfd, (struct sockaddr *)&address, len);
if (result == -1) {
perror(«oops: getdate»);
exit(1);
}
result = read(sockfd, buffer, sizeof(buffer));
buffer[result] = ' ';
printf(«read %d bytes: %s», result, buffer);
close(sockfd);
exit(0);
}
Вы можете применять программу getdate
для получения времени суток с любого известного узла сети.
$ ./getdate localhost
daytime port is 13
read 26 bytes: 24 JUN 2007 06:03:03 BST
$
Если вы получаете сообщение об ошибке, такое как
oops: getdate: Connection refused
или
oops: getdate: No such file or directory
причина может быть в том, что на компьютере, к которому вы подключаетесь, не включен сервис daytime
. Такое поведение стало стандартным для большинства современных систем Linux. В следующем разделе вы увидите, как включать этот и другие сервисы.
Как это работает
При выполнении данной программы можно задать узел, к которому следует подключиться. Номер порта сервиса daytime
определяется функцией сетевой базы данных getservbyname
, которая возвращает сведения о сетевых сервисах таким же способом, как и при получении информации об узле сети. Программа getdate
пытается соединиться с адресом, который указан первым в списке дополнительных адресов заданного узла. Если соединение успешно, программа считывает сведения, возвращаемые сервисом daytime, символьную строку, содержащую системные дату и время.
Системы UNIX, предоставляющие ряд сетевых сервисов, зачастую делают это с помощью суперсервера. Эта программа (интернет-демон xinetd или inetd) ожидает одновременно запросы на соединения с множеством адресов портов. Когда клиент подключается к сервису, программа-демон запускает соответствующий сервер. При таком подходе серверам не нужно работать постоянно, они могут запускаться по требованию.
Примечание
В современных системах Linux роль интернет-демона исполняет программа xinetd. Она заменила оригинальную UNIX-программу inetd, которую вы все еще можете встретить в более ранних системах Linux и других UNIX-подобных системах.
Программа xinetd обычно настраивается с помощью пользовательского графического интерфейса для управления сетевыми сервисами, но вы можете изменять и непосредственно файлы конфигурации программы. К ним относятся файл /etc/xinetd.conf и файлы в каталоге /etc/xinetd.d.
У каждого сервиса, предоставляемого программой xinetd, есть файл конфигурации в каталоге /etc/xinetd.d. Программа xinetd считает все эти файлы конфигурации во время запуска и повторно при получении соответствующей команды.
Далее приведена пара примеров файлов конфигурации xinetd, первый из них для сервиса daytime
.
# По умолчанию: отключен
# Описание: сервер daytime. Это версия tcp.
service daytime
{
socket_type = stream
protocol = tcp
wait = no
user = root
type = INTERNAL
id = daytime-stream
FLAGS = IPv6 IPv4
}
Следующий файл конфигурации предназначен для сервиса передачи файлов.
# По умолчанию: отключен
# Описание:
# FTP-сервер vsftpd обслуживает FTP-соединения. Он использует
# для аутентификации обычные, незашифрованные имена пользователей и
# пароли, vsftpd спроектирован для безопасной работы.
#
# Примечание: этот файл содержит конфигурацию запуска vsftpd для xinetd.
# Файл конфигурации самой программы vsftpd находится в
# /etc/vsftpd.conf
service ftp {
# server_args =
# log_on_success += DURATION USERID
# log_on_failure += USERID
# nice = 10
socket_type = stream
protocol = tcp
wait = no
user = root
server = /usr/sbin/vsftpd
}
Сервис daytime
, к которому подключается программа getdate
, обычно обрабатывается самой программой xinetd (он помечен как внутренний) и может включаться с помощью как сокетов типа SOCK_STREAM
(tcp), так и сокетов типа SOCK_DGRAM
(udp).
Сервис передачи файлов ftp
подключается только сокетами типа SOCK_STREAM
и предоставляется внешней программой, в данном случае vsftpd. Демон будет запускать эту внешнюю программу, когда клиент подключится к порту ftp
.
Для активизации конфигурационных изменений сервиса можно отредактировать конфигурацию xinetd и отправить сигнал отбоя (hang-up) процессу-демону, но мы рекомендуем использовать более дружелюбный способ настройки сервисов. Для того чтобы разрешить вашему клиенту подключаться к сервису daytime
, включите этот сервис с помощью средств, предоставляемых системой Linux. В системах SUSE и openSUSE сервисы можно настраивать из SUSE Control Center (Центр управления SUSE), как показано на рис. 15.1. У версий Red Hat (и Enterprise Linux, и Fedora) есть похожий интерфейс настройки. В нем сервис daytime
включается для TCP– и UDP-запросов.
Рис. 15.1
Для систем, применяющих программу inetd вместо xinetd, далее приведено эквивалентное извлечение из файла конфигурации inetd, /etc/inetd.conf, которое программа inetd использует для принятия решения о запуске серверов:
#
#
#
# Echo, discard, daytime и chargen используются в основном для
# тестирования.
#
daytime stream tcp nowait root internal
daytime dgram udp wait root internal
#
# Это стандартные сервисы.
#
ftp stream tcp-nowait root /usr/sbin/tcpd /usr/sbin/wu.ftpd
telnet stream tcp nowait root /usr/sbin/tcpd /usr/sbin/in.telnetd
#
# Конец файла inetd.conf.
Обратите внимание на то, что в нашем примере сервис ftp предоставляется внешней программой wu.ftpd. Если в вашей системе выполняется демон inetd, вы можете изменить набор предоставляемых сервисов, отредактировав файл /etc/inetd.conf (знак # в начале строки указывает на то, что это строка комментария) и перезапустив процесс inetd. Сделать это можно, отправив сигнал отбоя (hang-up) с помощью команды kill
. Для облегчения этого процесса некоторые системы настроены так, что программа inetd записывает свой ID в файл. В противном случае можно применить команду killall
:
# killall -HUP inetd
Существует много параметров, которые можно применять для управления поведением соединений на базе сокетов – слишком много для подробного описания в этой главе. Для манипулирования параметрами используют функцию setsockopt
:
#include
int setsockopt(int socket, int level, int option_name,
const void *option value, size_t option len);
Задавать параметры можно на разных уровнях иерархии протоколов. Для установки параметров на уровне сокета вы должны задать level
равным SOL_SOCKET
. Для задания параметров на более низком уровне протоколов (TCP, UDP и т.д.) приравняйте параметр level номеру протокола (полученному либо из заголовочного файла netinet/in.h, либо из функции getprotobyname
).
В аргументе option_name
указывается имя задаваемого параметра, аргумент option_value
содержит произвольное значение длиной option_len
байтов, передаваемое без изменений обработчику низкоуровневого протокола.
Параметры уровня сокета определены в заголовочном файле sys/socket.h и включают приведенные в табл. 15.4 значения.
Таблица 15.5
SO_DEBUG | Включает отладочную информацию |
SO_KEEPALIVE | Сохраняет активными соединения при периодических передачах |
SO_LINGER | Завершает передачу перед закрытием |
Параметры SO_DEBUG
и SO_KEEPALIVE
принимают целое значение option_value
для установки или включения (1) и сброса или выключения (0). Для параметра SO_LINGER
нужна структура типа linger
, определенная в файле sys/socket.h и задающая состояние параметра и величину интервала задержки.
Функция setsockopt
возвращает 0 в случае успеха и -1 в противном случае. На страницах интерактивного справочного руководства описаны дополнительные параметры и ошибки.
Множественные клиенты
До сих пор в этой главе вы видели, как применяются сокеты для реализации клиент-серверных систем, как локальных, так действующих, в сети. После установки соединения на базе сокетов они ведут себя как низкоуровневые открытые файловые дескрипторы и во многом как двунаправленные каналы.
Теперь необходимо рассмотреть случай множественных клиентов, одновременно подключающихся к серверу. Вы видели, что, когда серверная программа принимает от клиента запрос на соединение, создается новый сокет, а исходный сокет, ожидающий запросы на соединение, остается доступен для последующих запросов. Если сервер не сможет немедленно принять поступившие позже запросы на соединения, они сохранятся в очереди ожидания.
Тот факт, что исходный сокет все еще доступен, и что сокеты ведут себя как файловые дескрипторы, дает нам метод одновременного обслуживания многих клиентов. Если сервер вызовет функцию fork
для создания своей второй копии, открытый сокет будет унаследован новым дочерним процессом. Далее он сможет обмениваться данными с подключившимся клиентом, в то время как основной сервер продолжит прием последующих запросов на соединение. В действительности в вашу программу сервера нужно внести очень простое изменение, показанное в упражнении 15.7.
Поскольку вы создаете дочерние процессы, но не ждете их завершения, следует сделать так, чтобы сервер игнорировал сигналы SIGCHLD
, препятствуя возникновению процессов-зомби.
Упражнение 15.7. Сервер для многочисленных клиентов
1. Программа server4.c начинается так же, как последний рассмотренный сервер с важным добавлением директивы include
для заголовочного файла signal.h. Переменные и процедуры создания и именования сокета остались прежними:
#include
#include
#include
#include
#include
#include
#include
int main() {
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(9734);
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
2. Создайте очередь соединений, игнорируйте подробности завершения дочернего процесса и ждите запросов клиентов:
listen(server_sockfd, 5);
signal(SIGCHLD, SIG_IGN);
while(1) {
char ch;
printf(«server waitingn»);
3. Примите запрос на соединение:
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,
(struct_sockaddr*)&client_address, &client_len);
4. Вызовите fork
с целью создания процесса для данного клиента и выполните проверку, чтобы определить, родитель вы или потомок:
if (fork() == 0) {
5. Если вы потомок, то можете читать/писать в программе-клиенте на сокете client_sockfd
. Пятисекундная задержка нужна для того, чтобы это продемонстрировать:
read(client_sockfd, &ch, 1);
sleep(5);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
exit(0);
}
6. В противном случае вы должны быть родителем и ваша работа с данным клиентом закончена:
else {
close(client_socket);
}
}
}
Код включает пятисекундную задержку при обработке запроса клиента для имитации вычислений сервера или обращения к базе данных. Если бы вы проделали это в предыдущем сервере, каждое выполнение программы client3 заняло бы пять секунд. С новым сервером вы сможете обрабатывать множественные клиентские программы client3 параллельно с общим затраченным временем, чуть превышающим пять секунд.
$ ./server4 &
[1] 26566 server waiting
$ ./client3 & ./client3 & ./client3 & ps x
[2] 26581
[3] 26582
[4] 26583
server waiting
server waiting
server waiting
PID TTY STAT TIME COMMAND
26566 pts/1 S 0:00 ./server4
26581 pts/1 S 0:00 ./client3
26582 pts/1 S 0:00 ./client3
26583 pts/1 S 0:00 ./client3
26584 pts/1 R+ 0:00 ps x
26585 pts/1 S 0:00 ./server4
26586 pts/1 S 0:00 ./server4
26587 pts/1 S 0:00 ./server4
$ char from server = В
char from server = В
char from server = В
ps x
PID TTY STAT TIME COMMAND
26566 pts/1 S 0:00 ./server4
26590 pts/1 R+ 0:00 ps x
[2] Done ./client3
[3]– Done ./client3
[4]+ Done ./client3
$
Как это работает
Теперь серверная программа создает новый дочерний процесс для обработки каждого клиента, поэтому вы можете видеть несколько сообщений об ожидании сервера, поскольку основная программа продолжает ждать новые запросы на подключения. В выводе команды ps
(отредактированном) показан главный процесс server4 с PID, равным 26 566, который ожидает новых клиентов, в то время, как три клиентских процесса client3 обслуживаются тремя потомками сервера. После пятисекундной паузы все клиенты получают свои результаты и завершаются. Дочерние серверные процессы тоже завершаются, оставляя только один главный серверный процесс.
Серверная программа применяет вызов fork
для обработки множественных клиентов. В приложении для работы с базой данных это может быть не самым удачным решением, т.к. серверная программа может быть довольно большой, и, кроме того, существует проблема координации обращений к базе данных множественных копий сервера. На самом деле, все, что вам нужно, – это способ обработки множественных клиентов единственным сервером без блокировки и ожидания доставки клиентских запросов. Решение этой задачи включает одновременную обработку множественных открытых файловых дескрипторов и не ограничено только приложениями с применением сокетов. Рассмотрим функцию select
.