Варианты заданий (1114804), страница 2
Текст из файла (страница 2)
§1), и проводить игру в соответствии справилами.— При проведении жеребьевок сервер не должен давать преимуществникому из игроков.— Сервер должен быть защищен от неправильных действий игроков,т.е. никакие действия одного игрока не должны нарушать ход игры.— Сервер должен обеспечивать реальный многопользовательский режим работы, т.е. никакие действия игрока не должны приводить, даже кратковременно, к невозможности для остальных игроков вводитькоманды и получать результаты, если только это не предусмотреноправилами игры.— Сервер не должен использовать активное ожидание. В частности, этоозначает, что в отсутствие активности игроков сервер не должен создавать никакой нагрузки на процессор. См.
§2.3.— Сервер обязан корректно обрабатывать потерю соединения с любымиз игроков. Игрок, соединение с которым оборвалось, считается выбывшим из игры, о чем сообщается остальным игрокам.Начать программирование следует с написания действующей моделисервера, в которой вместо игры фигурирует простейшая информационнаясущность – глобальная целочисленная переменная, которую может изменить каждый из клиентов. В этой главе приведены все необходимые дляэтого сведения.2.2Организация TCP-сервераВсе сетевые взаимодействия в операционных системах семейcтва Unix организованы с помощью так называемых сокетов (sockets). С каждым сокетомсвязывается файловый дескриптор, позволяющий ссылаться на сокет привыполнении операций с ним.При организации многопользовательского сервера понадобится два вида сокетов.
Первый из них – слушающий сокет (listening socket) будетиспользоваться для ожидания и приема клиентских соединений. Сокетыдругого вида представляют собой непосредствено “канал связи” с конкретным клиентом и используются для приема команд от клиентов и передачиклиентам сообщений сервера.92.2.1Создание сокетаПрежде всего, сокет (как объект ядра операционной системы) необходимосоздать вызовом socket():#include <sys/types.h>#include <sys/socket.h>int socket(int domain, int type, int protocol);где domain задает семейство адресации (address family), type задает типкоммуникации, protocol - конкретный протокол.В рассматриваемой задаче необходим сокет, работающий в семействеIP-адресов1 (задается константой AF_INET).
Это означает, что адрес сокета будет состоять из IP-адреса и номера порта. IP-адрес состоит из четырех чисел от 0 до 255, обычно записываемых через точку, например:192.168.15.131. Номер порта - это целое число в диапазоне от 1 до 65535.Следует учитывать, что в большинстве операционных системпорты с номерами от 1 до 1023 считаются привилегированными; это означает, что использовать их могут только процессы,обладающие правами суперпользователя.Среди существующих типов коммуникации для рассматриваемой задачи наиболее подходит так называемый потоковый (stream), задаваемыйконстантой SOCK_STREAM. Сокеты этого типа коммуникации представляютсобой двунаправленный канал, доступный на обоих концах как на запись,так и на чтение, в том числе и с помощью обычных вызовов read() иwrite().Наконец, в качестве параметра protocol можно указать 0, в результате чего система автоматически выберет единственный возможный дляданной комбинации семейства адресов и типа сокета протокол TCP (иначезадаваемый константой IPPROTO_TCP).Таким образом, окончательно вызов будет выглядеть так:ls = socket(AF_INET, SOCK_STREAM, 0);где ls - имя переменной типа int, которой будет присвоен номер файловогодескриптора, ассоциированного с вновьсозданным сокетом.Полученный файловый дескриптор должен быть неотрицательным числом.
Если вызов socket() вернул значение −1, это свидетельствует о происшедшей ошибке. Программа обязательно должна корректно обрабатывать такую ситуацию.1Здесь и далее имеются в виду IP-адреса семейства протоколов IPv4.102.2.2Связывание сокета с адресомСледующим шагом развертывания сервера является сопоставление созданному сокету конкретного адреса. Напомним, что в избранном нами семействе адресов (AF_INET) адресом является пара “IP-адрес + порт”.Компьютер, оснащенный стеком протоколов TCP/IP, может иметь произвольное количество ip-адресов. В частности, любой компьютер имеет адрес 127.0.0.1, означающий сам этот компьютер и доступный только программам, работающим на этом же компьютере. При подключении к локальной сети компьютер также получает интерфейсный адрес для работыв этой сети.Сервер может принимать соединения по заданному номеру порта наодном из IP-адресов, имеющихся в системе, либо на всех IP-адресах сразу.Последнее задается IP-адресом 0.0.0.0, имеющим специальное значение иобозначающимся также константой INADDR_ANY.Связывание сокета с конкретным адресом производится вызовомbind():#include <sys/types.h>#include <sys/socket.h>int bind(int sockfd, struct sockaddr *addr, int addrlen);где sockfd – дескриптор сокета, полученный в результате выполнения вызова .socket().; addr – указатель на структуру, содержащую адрес; наконец,addrlen – размер структуры адреса в байтах.Реально в качестве параметра addr используется не структура типаsockaddr, а структура другого типа, который зависит от используемогосемейства адресации (см.
§2.2.1). В избранном нами семействе AF_INETиспользуется структура struct sockaddr_in, умеющая хранить пару “IPадрес + порт”. Эта структура имеет следующие поля:— sin_family – обозначает семейство адресации (в данном случае значение этого поля должно быть установлено в AF_INET).— sin_port – задает номер порта в сетевом порядке байт, который,вообще говоря, может отличаться от порядка байт, используемого наданной машине. Соответственно, значение для занесения в это поледолжно быть получено из выбранного номера порта вызовом функции htons()2.
Напомним, что номер порта задается как параметркомандной строки программы-сервера.2Название функции htons() получено как сокращение от Host to Network Short, т.е. преобразованиеиз хостового в сетевой порядок байт для короткого целого. Более подробно понятие сетевого порядкабайт будет рассмотрено в §2.6.111— sin_addr – задает IP-адрес. Поле sin_addr само является структурой, имеющей лишь одно поле с именем s_addr, которое хранит IPадрес в виде беззнакового четырехбайтного целого. Именно этому полю следует присвоить значение INADDR_ANY.Вызов bind() возвращает 0 в случае успеха, −1 – в случае ошибки.Учтите, что существует множество ситуаций, в которых вызов bind() может не пройти; например, ошибка произойдет в случае попытки использования привилегированного номера порта (от 1 до 1023) или порта, которыйна данной машине уже кем-то занят (возможно, другой вашей программой).
Поэтому обработка ошибок при вызове bind() особенно важна.Итак, окончательно подготовка и вызов bind() могут выглядеть следующим образом:struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = INADDR_ANY;if (0 != bind(ls, (struct sockaddr *) &addr, sizeof(addr))){/* Здесь следует поместить обработку ошибки */}где ls – переменная, хранящая дескриптор сокета, а port – переменная, вкоторую тем или иным способом занесен избранный номер порта.2.2.3Ожидание и прием клиентских соединенийПосле того, как сокет создан и с ним связан адрес, его необходимо перевести в состояние ожидания запросов на соединения, или, иначе говоря, вслушающий режим (listening state).
Это достигается вызовом listen():#include <sys/socket.h>int listen(int sockfd, int qlen);Параметр sockfd задает дескриптор сокета. Параметр qlen означаетмаксимальную длину очереди пришедших запросов на соединение, которые сервер еще не принял к обработке. Некоторые операционные системыне поддерживают значения qlen> 5, поэтому обычно вторым параметромвызова listen() задают просто число 5.Вызов listen() возвращает 0 в случае успеха, −1 – в случае ошибки.С учетом этого вызов может выглядеть так:12if (-1 == listen(ls, 5)) {/* Здесь следует поместить обработку ошибки */}В результате выполнения вызова listen() в системе появится слушающий сокет, который можно увидеть с помощью командыnetstat -a -n | grep LISTENКлиент, находящийся на любой машине, с которой доступна наша сеть,может установить соединение с нашим сокетом.
Чтобы получить возможность обмена информацией с этим клиентом одновременно с ожиданиемзапросов на соединение от других клиентов, нам необходимо принять запрос на соединение. Это делается с помощью вызова accept():#include <sys/types.h>#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, int *addrlen);Параметр sockfd задает дескриптор слушающего сокета, на котором следует принять соединение. Что касается параметров addr и addrlen, то онипозволяют узнать, с какого адреса исходит соединение. Информация записывается в структуру, на которую указывает указатель addr.
Переменная,на которую указывает addrlen, должна перед обращением к accept() содержать длину структуры addr в байтах; после обращения в ней будетсодержаться реальная длина данных, записанных в эту структуру. Естественно, использовать следует структуру типа struct sockaddr_in (см.§2.2.2).Если информация об источнике соединения не интересна, в качествеобоих параметров addr и addrlen можно передать нулевые указатели.Если во входной очереди уже имеется запрос на соединение, вызовaccept() возвращает управление немедленно; в противном случае управление будет возвращено после того, как такой запрос будет получен.Вызов accept() возвращает файловый дескриптор, связанный с сокетом, через который будет осуществляться связь с клиентом, соединениекоторого только что было принято.Полученный файловый дескриптор должен быть неотрицательным числом.