Варианты заданий (1114804), страница 3
Текст из файла (страница 3)
Если вызов socket() вернул значение −1, это свидетельствует о происшедшей ошибке. Программа обязательно должна корректно обрабатывать такую ситуацию.132.3Мультиплексирование ввода-выводаПосле того, как вызов accept() успешно отработает в первый раз, в вашейпрограмме появятся два файловых дескриптора, требующих внимания.Это слушающий сокет и сокет, полученный в результате вызова accept()(сокет клиента). На слушающий сокет могут поступить новые запросы, которые необходимо принимать вызовом accept(); в то же время на сокетклиента могут поступить данные, переданные клиентом, которые необходимо прочитать с помощью вызова read().
Какое из этих событий произойдетраньше, неизвестно.Более того, в программе могут быть и другие источники событий. Так,клиентов, скорее всего, будет больше одного. Кроме того, для реализацииуправления сервером нам, возможно, потребуется организовать диалог спользователем, запустившим сервер, для чего необходима возможность обработки данных, поступающих со стандартного ввода программы-сервера.2.3.1Методы организации многопользовательского сервераСуществует несколько подходов к организации программ, работающих снесколькими источниками событий. Самый простой (и некорректный) изних состоит в организации циклического опроса всех имеющихся источников событий. Так, все имеющиеся сокеты можно перевести в так называемый неблокирующий режим, в котором все системные вызовы, относящиесяк таким сокетам, которые не могут быть исполнены без блокирования выполнения процесса, будут возвращать управление сразу же, сигнализируяоб ошибке (в частности, вызов accept(), будучи вызванным в отсутствиенеобработанного запроса на соединение, немедленно вернет −1).Этот способ имеет фундаментальный неустранимый недостаток, заключающийся в наличии так называемого активного ожидания.
Действительно, даже если не происходит никаких событий, требующих обработки,программа-сервер будет вновь и вновь опрашивать имеющиеся дескрипторы, впустую занимая процессорное время. Естественно, пользоваться таким методом ни в коем случае не следует.Второй вариант организации многопользовательского сервера, о котором, в частности, рассказывается в основном курсе лекций “Системное программное обеспечение” в III семестре, основан на создании отдельного процесса для работы с каждым источником событий. В этом случае послекаждого вызова accept() немедленно выполняется вызов fork(), порождающий новый процесс, и родительский процесс возвращается к обработке входящих запросов на соединение, в то время как дочерний процесс14обслуживает клиента, используя полученный от accept() дескриптор.
Подробно этот механизм рассмотрен в книге [2].Такой вариант идеально подходит для случая, когда каждый клиентобслуживается отдельно от остальных и не имеет с ними никакой связи.Однако для случая многопользовательской игры, которая проходит в общем игровом пространстве, такой способ подходит заметно хуже, посколькувлечет активное использование разделяемой памяти и семафоров, что самопо себе усложняет программу3 .Третий способ называется мультиплексированием ввода-вывода иможет быть осуществлен с помощью системных вызовов select()или poll()4.
В дальнейшем мы ограничимся рассмотрением функцииselect(). При желании читатель может освоить функцию poll() самостоятельно, прибегнув к литературе [3] и команде man.2.3.2Вызов select()Системный вызов select() предназначен для использования в ситуации,когда необходимо организовать работу с несколькими файловыми дескрипторами, не имея a priori информации о том, какой из дескрипторов первымпотребует внимания программы.
Кроме того, возможно, требуется отслеживание некоторых событий по времени (например, тайм-аутов на сетевыхсоединениях).Прототип вызова select() выглядит следующим образом:#include <sys/time.h>#include <sys/types.h>#include <unistd.h>int select(int n, fd_setfd_setfd_setstruct*readfds,*writefds,*exceptfds,timeval *timeout);Параметры readfds, writefds и exceptfds обозначают множества файловых дескрипторов, для которых нас интересует, соответственно, возмож3Тем не менее, построить сервер таким образом вполне возможно. Несмотря на сложности, связанные с использованием разделяемой памяти, это может быть интересно в качестве упражнения.4Вообще говоря, select() и poll() предназначены для одних и тех же действий. select() несколькопроще в работе, poll() несколько более универсален.
В некоторых системах ядро реализует только одинвариант интерфейса, при этом второй эмулируется через него в виде библиотечной функции. Так,в системе Solaris присутствует системный вызов poll(), а select() является библиотечной функцией.Кроме того, в некоторых современных системах присутствует также вызов kqueue(), реализующийальтернативный подход к выборке события.15ность немедленного чтения, возможность немедленной записи и наличиеисключительной ситуации. Параметр n указывает, какое количество элементов в этих множествах является значащим.
Этот параметр необходимоустановить равным max_d+1, где max_d – максимальный номер дескриптора среди подлежищих обработке. Наконец, параметр timeout задает промежуток времени, спустя который следует вернуть управление, даже еслиникаких событий, связанных с дескрипторами, не произошло.Объект “множество дескрипторов” задается переменной типа fd_set.Внутренняя реализация переменных этого типа нас, вообще говоря, не интересует5. Для работы с переменными этого типа система предоставляет внаше распоряжение следующие макросы:FD_ZERO(fd_set *set);/*FD_CLR(int fd, fd_set *set);/*FD_SET(int fd, fd_set *set);/*FD_ISSET(int fd, fd_set *set);/*очистить множество */убрать дескриптор из мн-ва */добавить дескриптор к мн-ву */входит ли дескр-р в мн-во? */В рассматриваемой задаче объемы передаваемых по сети данных сравнительно незначительны, что позволяет предполагать, что системные вызовы, осуществляющие запись в сокеты, никогда не будут блокироватьпрограмму-сервер.
Также можно считать, что на сокетах никогда не произойдут исключительные ситуации. Таким образом, аргументы writefds иexceptfds можно не использовать (вместо них передавать вызову select()нулевые указатели).В простейшей версии программы-сервера также нет необходимости виспользовании параметра timeout6. Поэтому можно передать нулевой указатель и в качестве пятого параметра вызова. На всякий случай отметим, чтоструктура timeval имеет два поля типа long. Поле tv_sec задает количество секунд,поле tv_usec - количество микросекунд (миллионных долей секунды).Вызов select() изменяет все переданные ему по указателюаргументы, так что перед каждым обращением к нему аргументыдолжны быть сформированы заново.Вызов select() возвращает −1 в случае возникновения ошибки. Учтите,что, если ваша программа обрабатывает те или иные сигналы, вызов select() можетвернуть −1 в случае, если его выполнение было прервано пришедшим сигналом.
Приэтом значением переменной errno будет EINTR, что свидетельствует о нормальном ходе событий. Вы можете не учитывать данный комментарий, если ваша программа не5Для различных систем она может оказаться разной.Необходимость использования этого параметра возникает при выполнении некоторых дополнительных задач.616перехватывает никаких сигналов. Вызов возвращает значение 0 в случае, еслипричиной выхода из вызова стало наступление заданного таймаута. Есливызов возвратил положительное число, оно означает количество дескрипторов, для которых произошло какое-то событие.После возврата из вызова select() переданные ему переменные типаfd_set оказываются модифицированы.
Если при входе в вызов эти множества содержали дескрипторы, относительно которых нас интересует информация о событиях, то по окончании вызова эти же множества содержатдескрипторы, на которых событие реально произошло (например, по сетиприбыли данные, которые могут быть считаны).Таким образом, работу с вызовом select() можно построить по следующей схеме (считаем, что номер слушающего сокета по-прежнему хранитсяв переменной ls; как хранить дескрипторы клиентских сокетов, читателюпредлагается решить самостоятельно):for(;;) { /* главный цикл */fd_set readfds;int max_d = ls;/* изначально полагаем, что максимальным являетсяномер слушающего сокета */FD_ZERO(&readfds); /* очищаем множество */FD_SET(ls, &readfds);/* вводим в множестводескриптор слушающего сокета */int fd;/* организуем цикл по сокетам клиентов */for(fd=/*дескриптор первого клиента*/ ;/*клиенты еще не исчерпаны?*/;fd=/*дескриптор следующего клиента*/) {/* здесь fd - очередной клиентский дескриптор *//* вносим его в множество */FD_SET(fd, &readfds);/* проверяем, не больше ли он,нежели текущий максимум */if(fd > max_d) max_d = fd;}int res = select(max_d+1, &readfds, NULL, NULL, NULL);if(res < 1) {/* обработка ошибки, происшедшей в select()’е */}if(FD_ISSET(ls, &readfds)) {17/* пришел новый запрос на соединение *//* здесь его необходимо принятьвызовом accept() и запомнитьдескриптор нового клиента */}/* теперь перебираем все клиентские дескрипторы */for(fd=/*дескриптор первого клиента*/ ;/*клиенты еще не исчерпаны?*/;fd=/*дескриптор следующего клиента*/)if(FD_ISSET(fd, &readfds)) {/* пришли данные от клиента с сокетом fd *//* читаем их вызовом \verb.read().
или\verb.recv(). и обрабатываем */}/* конец главного цикла, можно идти наследующую итерацию */}2.42.4.1Прием и передача данных через сокетыЧтениеЧтение данных из сокета можно произвести обычным вызовом read(), ужезнакомым читателю из курса “Системное программное обеспечение”:#include <unistd.h>size_t read(int fd, void *buf, size_t len);либо специально предназначенным для сокетов вызовом recv():#include <sys/types.h>#include <sys/socket.h>size_t recv(int fd, void *buf, size_t len,unsigned int flags);В обоих случаях fd задает файловый дескриптор (в случае recv() это обязательно должен быть дескриптор сокета); buf указывает на буфер, в который следует поместить прочитанные данные; len сообщает вызову размербуфера, чтобы избежать его переполнения.