А.В. Столяров - Практикум на ЭВМ - Многопользовательский игровой сервер (1114951), страница 2
Текст из файла (страница 2)
В этом случае необходимо реализовать клиентскую программу, ведущую диалог с пользователеми формирующую соответствующие данные для сервера, а также преобразующую получаемые от сервера ответы в приемлемую для пользователя форму.Стартовыми параметрами сервера являются число игроков и номерTCP-порта, на котором программа должна ожидать запросы на соединениеот клиентов. Стартовые параметры задаются в командной строке сервера.По согласованию с преподавателем возможны другие варианты получениястартовых параметров, например, из конфигурационного файла или из переменных окружения. Задавать стартовые параметры в тексте программы(т.е.
так, что их изменение потребует перекомпиляции программы) запрещается.После запуска программы-сервера она должна открыть сокет в режимеожидания запросов на соединение (см. §2.2) на заданном порту, дождатьсяподключения заданного количества игроков, после чего перейти в режимигры, в котором и оставаться до момента, когда все игроки, кроме одного,по тем или иным причинам не выйдут из игры.До тех пор, пока сервер не перешел в режим игры, на любую командуигрока он должен реагировать сообщением о том, что игра не началась;желательно также выдавать при этом информацию о том, сколько игроковв настоящее время уже вошли на сервер и сколько еще ожидается до началаигры.После перехода в режим игры при попытке нового игрока подключитьсяк серверу он должен получить сообщение о том, что игра уже идет, послечего сервер должен разорвать соединение.8Требования к серверу:— Сервер должен предоставлять игроку все возможности, предусмотренные правилами игры (см. §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).