netlib.narod.ru | < Назад | Оглавление | Далее > |
Я люблю учить на примерах, так что как насчет программы, использующей TCP/IP, которая подключается к Интернету, посылает HTTP-запрос на Web-сервер и отображает главную страницу сайта? Перед тем, как я перейду к коду, посмотрите на рис. 14.10, где показано что именно мы будем делать.
Рис. 14.10. Ход выполнения простой программы, использующей сокеты
Здесь вы можете видеть этапы, необходимые для того, чтобы подключиться к Веб-серверу и загрузить с него главную страницу. Сперва вы инициализируете сокеты, чтобы коммуникационный уровень был готов к работе. Затем вы создаете сокет, который будет использоваться для подключения к Web-серверу. Когда сокет готов, вы находите IP-адрес Web-сервера и устанавливаете соединение с ним. После установки соединения вы отправляете HTTP-запрос на получение содержимого главной страницы. После этого вам остается только ждать, когда запрошенная информация придет в буфер ответа. Получив данные вы закрываете сокет и отключаете всю систему сокетов.
Я реализовал код, необходимый для воссоздания этапов, изображенных на рис. 14.10. Загрузите программу Sockets_Receive и следуйте за мной. Проект состоит из файла main.cpp и единственной библиотеки ws2_32.lib, которая содержит все, что необходимо для программирования сокетов в Windows. Скомпилируйте программу и запустите ее. Вы увидите окно консольного приложения, в котором отображается содержимое главной страницы сайта, имя которого задано в коде. Как это должно выглядеть, показано на рис. 14.11.
Рис. 14.11. Окно программы Sockets_Receive
Открыв файл main.cpp вы увидите следующий код:
#include <iostream.h> #include <winsock.h> #include <stdio.h> void main(void) { SOCKET skSocket; sockaddr_in saServerAddress; int iPort = 80; int iStatus; WSADATA wsaData; WORD wVersionRequested; LPHOSTENT lpHost; char szHost[128]; char szSendBuffer[256]; char szRecvBuffer[32768]; int iBytesSent; int iBytesReceived; sprintf(szHost,"www.lostlogic.com"); // Сообщаем WinSock, что нам нужна версия 2 wVersionRequested = MAKEWORD(2, 0); // Инициализируем дескриптор сокета skSocket = INVALID_SOCKET; // Запускаем WinSock iStatus = WSAStartup(wVersionRequested, &wsaData); // Создаем сокет skSocket = socket(AF_INET, SOCK_STREAM, 0); // Проверяем наличие ошибок if(skSocket == INVALID_SOCKET) { cout << "**ERROR** Could Not Create Socket" << endl; exit(1); } memset(&saServerAddress, 0, sizeof(sockaddr_in)); saServerAddress.sin_family = AF_INET; saServerAddress.sin_addr.s_addr = inet_addr(szHost); if(saServerAddress.sin_addr.s_addr == INADDR_NONE) { lpHost = gethostbyname(szHost); if (lpHost != NULL) { // Получаем адрес сервера из информации хоста saServerAddress.sin_addr.s_addr = ((LPIN_ADDR)lpHost->h_addr)->s_addr; } else { cout << "**ERROR** Could Not Locate Host" << endl; exit(1); } } // Задаем порт сервера saServerAddress.sin_port = htons(iPort); // Пытаемся подключиться к серверу iStatus = connect(skSocket, (struct sockaddr*)&saServerAddress, sizeof(sockaddr)); // Проверяем наличие ошибок if(iStatus == SOCKET_ERROR) { cout << "**ERROR** Could Not Connect To Server" << endl; exit(1); } sprintf(szSendBuffer,"GET / HTTP/1.0\n\n"); // Отправляем HTTP-запрос iBytesSent = send(skSocket, szSendBuffer, 256, 0); memset(szRecvBuffer, 0x00, 32768); // Получаем данные iBytesReceived = recv(skSocket, szRecvBuffer, 32768, 0); cout << szRecvBuffer << endl; // Завершаем работу closesocket(skSocket); WSACleanup(); }
Заголовочный файл winsock.h содержит всю необходимую информацию для работы с библиотекой сокетов ws2_32.lib. Убедитесь, что включаете его в любой ваш код, который работает с сокетами. Остальные заголовочные файлы применяются ежедневно в обычном программировании.
Перед тем, как вы вообще сможете использовать какие-либо сокеты, необходимо инициализировать систему сокетов Windows, вызвав функцию WSAStartup(). Эта функция получает номер версии сокетов, которую вы намереваетесь использовать и инициализирует коммуникационную систему. Раз вы хотите использовать сокеты версии 2, установите номер запрашиваемой версии равным 2.
Чтобы подключиться к внешнему миру вам нужен канал связи в виде сокета. Чтобы создать такой канал вызовите функцию socket(), предоставляемую библиотекой сокетов. Успешно завершившаяся функция возвращает идентификационный номер (дескриптор) сокета.
Если вы хотите установить соединение с сервером, используя URL, а не IP-адрес, вам сперва надо будет найти IP-адрес по URL. Это делается с помощью функции gethostbyname(). Она получает имя сервера и преобразует его в соответствующий IP-адрес.
Сервер может принимать подключения по нескольким линиям связи. Каждая из таких линий называется портом. Поскольку предоставляется несколько портов на выбор, необходимо указывать конкретный порт, с которым вы хотите соединиться. Это делается путем указания номера порта во время инициализации структуры адреса сервера sockaddr_in. Требуемая информация содержится в поле sin_port вышеупомянутой структуры. Задайте значение переменной sin_port и вы готовы идти дальше.
После того, как заданы IP-адрес и порт, вы можете подключаться к серверу. Это делается путем вызова предоставляемой сокетом функции connect(). Ей передаются дескриптор сокета, который вы хотите использовать, и параметры сервера к которому вы хотите подключиться. Если функция возвращает значение SOCKET_ERROR, значит произошла ошибка; в противном случае соединение установлено.
Теперь, когда соединение с сервером установлено, можно отправить пакет с HTTP-запросом. Этот пакет сообщает серверу, что вы хотите увидеть содержимое предоставляемой по умолчанию веб-страницы. Для отправки пакета необходимо воспользоваться функцией send(). Она получает сокет, через который будут отправлены данные, сами отправляемые данные и их размер. В рассматриваемом примере я отправляю содержимое буфера szSendBuffer через сокет, идентификатор которого хранится в переменной skSocket.
Если какие-либо данные были переданы, функция возвращает количество отправленных байт.
После того, как вы отправили HTTP-запрос серверу, следует получить ответ от него. Чтобы увидеть ответ, вы должны вызвать функцию recv(), которая вернет данные из буфера связи сокета. Самое лучшее в сокетах то, что они автоматически принимают данные и помещают их в системный буфер, так что вам не следует беспокоиться, что данные могут быть потеряны из-за того, что ваша программа занята. Тем не менее, следует проявлять осторожность и не ждать слишком долго, поскольку данные, которые находятся в буфере слишком долго будут потеряны.
Функции приема в параметрах передаются идентификатор сокета, от которого вы хотите получить информацию, буфер для размещения данных и его размер. Как только появятся какие-нибудь данные для получения, они будут переданы в буфер приема и программа продолжит работу. Если данные никогда не будут отправлены, функция будет ждать вечно, пока вы не завершите программу. Такова природа синхронных сокетов (blocking socket).
После того, как функция приема данных вернет управление программе, вы можете взглянуть на содержимое веб-страницы, выведя на экран буфер szRecvBuffer.
Теперь, когда работа с сокетом завершена, необходимо закрыть его. Это делает функция closesocket(). Она получает идентификатор сокета, который будет закрыт и отключен.
Когда вы полностью завершили работу с сокетами, необходимо отключить коммуникационную систему сокетов, вызвав функцию WSACleanup(). Ее требуется вызывать один раз в конце вашей программы.
Мы вихрем промчались по теме синхронных сокетов! Я знаю что действительно сильно разогнался, но моей главной целью было заложить основы для более интересного материала.
Как вы знаете, есть два типа стратегических игр: походовые и реального времени. Хотели ли вы когда-нибудь создать походовую игру, в которую можно играть через Интернет? Я говорю о том, что коллективные игры доставляют много удовольствия, но не слишком удобны, потому что игрокам необходимо собраться в одном месте. Здесь на сцену выходит программа, которую я сейчас опишу. В сопроводительных файлах к книге есть проект Sockets_TurnGame. Будучи скомпилированной, эта программа демонстрирует как осуществляется походовая игра в локальной сети или через Интернет. Пойдемте дальше и загрузим этот проект.
Походовые сетевые игры следуют очень прямолинейной схеме работы. Чтобы узнать, как она выглядит, взгляните на рис. 14.12.
Рис. 14.12. Ход выполнения походовой сетевой игры
На иллюстрации изображены клиент и сервер для походовой сетевой игры. Процесс начинается с запуска сервера, который прослушивает указанный порт, ожидая подключения клиента. Как только клиент установил соединение, сервер принимает его и ждет, пока клиент сделает свой ход. Как только клиент будет готов, он посылает пакет с данными своего хода серверу и ждет ответного хода сервера. Когда игрок на сервере будет готов, он завершает свой ход и отправляет пакет с данными хода клиенту. Этот процесс повторяется до завершения игры. Получается игра в которой вы получаете право хода, делаете ход и затем ждете, пока другой игрок сделает то же самое.
Программа Sockets_TurnGame предлагает пример реализации схемы, изображенной на рис. 14.12. Запустите программу и вы увидите окно, изображенное на рис. 14.13.
Рис. 14.13. Окно программы Sockets_TurnGame
На рис. 14.13 изображено небольшое окно с элементами управления, позволяющими работать как главный компьютер или установить соединение. Щелчок по кнопке Host переключает программу в режим игрового сервера, а щелчок по кнопке Connect переключает программу в режим клиента. Так как нельзя получить цыпленка раньше яйца, вы должны сначала запустить программу главного компьютера и уже потом подключаться к ней посредством клиента.
Если вы до сих пор не поняли, чтобы программа работала должным образом, вам необходимо запустить ее дважды. Это необходимо потому, что для демонстрации передачи ходов туда и обратно необходим как ведущий компьютер, так и клиент. Если вы еще не сделали этого, запустите программу дважды и в одном из экземпляров щелкните по кнопке Host. После этого в другом экземпляре программы щелкните по кнопке Connect. В результате вы должны увидеть что-нибудь, напоминающее рис. 14.14.
Рис. 14.14. Клиент установил соединение с сервером
На рис. 14.14 и на вашем экране вы видите два экземпляра программы. Программа сервера должна ожидать, пока клиент сделает свой ход, и у программы клиента должна быть готовая к использованию кнопка Turn Done. Игрок, у которого видна кнопка Turn Done в данный момент получил контроль над игрой и может передать его другому игроку, щелкнув по кнопке. Так вы можете передавать ход туда и обратно, щелкая по появляющейся кнопке. Я знаю, что понадобится напрячь воображение, но попытайтесь представить себе, что между щелчками по кнопке вы выполняете сложные игровые ходы.
В работающую с сокетами программу я решил включить функции как для сервера, так и для клиента. Из-за этого код разветвляется в двух направлениях, в зависимости от того, какую роль выберет пользователь. Ход выполнения программы изображен на рис. 14.15.
Рис. 14.15. Ход выполнения программы Sockets_TurnGame
На иллюстрации показано, что программа начинает работу с инициализации элементов управления и сокетов. После того, как инициализация успешно завершена, программа переходит в цикл обработки сообщений и ждет, пока пользователь щелкнет по кнопке Host или Connect. Если пользователь выбрал кнопку Connect, программа ждет, пока он закончит свой ход. Если же пользователь выберет кнопку Host, программа ждет подключения клиента.
Код проекта содержится в файлах main.cpp и main.h. Для работы программе требуются две библиотеки: winmm.lib и ws2_32.lib. Библиотека winmm.lib не требуется для работы с сетью, я использую ее для воспроизведения звукового сигнала, когда пользователь щелкает по кнопке завершения хода.
Загрузите заголовочный файл main.h, и вы увидите в нем такой код:
// Переменные сокетов SOCKET g_skListenSocket; SOCKET g_skClientSocket; bool g_bIsServer = 0; bool g_bMyTurn = 0; bool g_bConnected = 0;
В приведенном выше коде объявлены два дескриптора сокетов. Ведущий компьютер для прослушивания сети в ожидании новых соединений использует дескриптор g_skListenSocket. Другой дескриптор, g_skClientSocket, используется клиентом для подключения к серверу или сервер назначает его подключившемуся клиенту. Так или иначе, клиентский сокет используется для управления соединением между клиентом и сервером.
Логическое значение g_bIsServer сообщает вам работает ли программа в режиме ведущего компьютера. Если значение равно 1, значит программа является сервером и должна ожидать подключения клиента. Если значение равно 0, программа является клиентом и должна установить соединение с ведущим игровым компьютером.
Логическое значение g_bMyTurn сообщает принадлежит ли вам в данный момент право хода в игре. Если сейчас ваш ход, будет отображаться кнопка Turn Done, щелкнув по которой вы передадите ход другому игроку. Если сейчас ваш ход, значение переменной равно 1, если нет — значение переменной равно 0.
Логическое значение g_bConnected сообщает вам установила ли программа соединение с другим игроком. 1 означает что соединение существует, 0 — что нет.
Есть и еще несколько глобальных переменных, но они относятся к элементам управления Windows и другим подобным вещам.
Далее в заголовочном файле main.h приведены прототипы используемых в программе функций. Вот они:
void vHost(); void vInitializeSockets(void); void vShutdownSockets(void); void vConnect(void); void vSendTurnMessage(void); void vReceiveTurnMessage(void); void vTurnDone(void);
Функция vHost() вызывается когда пользователь щелкает по кнопке Host. Она прослушивает указанный порт на ведущем компьютере и ждет входящих подключений. Как только клиент установит соединение, ведущий компьютер принимает его, после чего можно производить обмен данными.
Функция vInitializeSockets() используется для начальной инициализации WinSock.
Функция vShutdownSockets() отключает все активные соединения и систему WinSock.
Функция vConnect() вызывается когда пользователь щелкает по кнопке Connect. Она пытается подключиться к ведущему компьютеру, чей IP-адрес указан в окне. После того, как соединение установлено, клиент получает контроль над игрой и может закончить ход в выбранный им момент времени.
Функция vSendTurnMessage() отправляет сообщающий о завершении хода пакет другому игроку. На самом деле пакет не содержит никакой полезной информации, он просто показывает вам как пересылать данные по проводам.
Функция vReceiveTurnMessage() ждет, пока другой игрок не пришлет сообщающий о завершении хода пакет. Функция будет сидеть и ждать, пока пока рак на горе не свистнет.
Функция vTurnDone() вызывается функциями отправки и приема хода для завершения хода. Это происходит когда пользователь щелкает по кнопке Turn Done.
Остальные перечисленные в заголовочном файле main.h функции являются стандартным каркасом приложения для Windows и не слишком интересны, поэтому я не буду их рассматривать. Вы же не хотите, чтобы я по сто раз описывал одно и то же? Лучше я лишний раз сыграю в Age of Mythology!
Представьте на минуту, что вы загрузили программу походовой игры и щелкнули по кнопке Host. Начнет выполняться следующий код:
sockaddr_in saServerAddress; sockaddr_in saClientAddress; int iClientSize = sizeof(sockaddr_in); int iPort = 6001; int iStatus; // Установка глобальных переменных g_bIsServer = 1; // Инициализация дескриптора сокета g_skListenSocket = INVALID_SOCKET; // Создание сокета g_skListenSocket = socket(AF_INET, SOCK_STREAM, 0); // Проверим, не произошла ли ошибка if(g_skListenSocket == INVALID_SOCKET) { vShowText("** ERROR ** Could Not Create Socket"); return; } vShowText("<- Socket Created ->"); // Очищаем структуру адреса сокета memset(&saServerAddress, 0, sizeof(sockaddr_in)); // Инициализируем структуру адреса сокета saServerAddress.sin_family = AF_INET; saServerAddress.sin_addr.s_addr = htonl(INADDR_ANY); saServerAddress.sin_port = htons(iPort); // Пытаемся выполнить привязку if(bind(g_skListenSocket, (sockaddr*) &saServerAddress, sizeof(sockaddr)) == SOCKET_ERROR) { vShowText("** ERROR ** Could Not Bind Socket"); return; } vShowText("<- Socket Bound ->"); // Прослушиваем подключения iStatus = listen(g_skListenSocket, 32); if(iStatus == SOCKET_ERROR) { vShowText("** ERROR ** Could Not Listen"); // Закрываем сокет closesocket(g_skListenSocket); return; } vShowText("<- Socket Listening ->"); g_skClientSocket = accept(g_skListenSocket, (struct sockaddr*)&saClientAddress, &iClientSize); if(g_skClientSocket == INVALID_SOCKET) { vShowText("** ERROR ** Could Not Accept Client"); // Закрываем сокет closesocket(g_skListenSocket); return; } // Убираем кнопки DestroyWindow(hBU_Connect); DestroyWindow(hBU_Host); vShowText("<- Client Connected ->"); // Устанавливаем флаг подключения g_bConnected = 1; // Устанавливаем флаг, сообщающий, что сейчас // ход делает другой игрок g_bMyTurn = 0; // Ждем первый ход клиента vTurnDone();
Первая часть кода реализует логику для подключения клиента. Фактически программа прослушивает порт, ожидая соединения и принимает соединение, когда оно происходит. После этого код убирает кнопки Host и Connect, чтобы пользователь не мог щелкнуть по ним еще раз. Затем программа устанавливает переменную хода, чтобы она указывала, что контроль над игрой находится у клиента. И, наконец, ход заканчивается вызовом функции завершения хода. Это переводит сервер в режим приема, чтобы он мог получить сообщение о завершении хода от клиента. Все эти действия показаны на рис. 14.16.
Рис. 14.16. Ход выполнения функции vHost()
Функция vConnect() вызывается, когда игрок щелкает по кнопке Connect. Вот как выглядит ее код:
sockaddr_in saServerAddress; int iPort = 6001,iStatus; LPHOSTENT lpHost; char szHost[128]; // Установка глобальных переменных g_bIsServer = 0; // Инициализация параметров сервера, смените указанный здесь IP-адрес // на корректный IP-адрес вашей сети sprintf(szHost, "192.168.0.2"); // Инициализация дескриптора сокета g_skClientSocket = INVALID_SOCKET; // Создание сокета g_skClientSocket = socket(AF_INET, SOCK_STREAM, 0); // Проверка наличия ошибок if(g_skClientSocket == INVALID_SOCKET) { vShowText("** ERROR ** Could Not Create Socket"); return; } vShowText("<- Socket Created ->"); // Инициализация структуры данных адреса сервера memset(&saServerAddress, 0, sizeof(sockaddr_in)); // Установка значений по умолчанию saServerAddress.sin_family = AF_INET; // Загрузка IP-адреса saServerAddress.sin_addr.s_addr = inet_addr(szHost); // Если задано имя сервера, а IP-адрес отсутствует, // попытаемся получить требуемое значение if(saServerAddress.sin_addr.s_addr == INADDR_NONE) { vShowText("<- Looking Up Host ID ->"); // Получаем имя сервера lpHost = gethostbyname(szHost); // Проверяем, получили ли мы что-нибудь if (lpHost != NULL) { // Загружаем адрес сервера из его данных saServerAddress.sin_addr.s_addr = ((LPIN_ADDR)lpHost->h_addr)->s_addr; } else { vShowText("** ERROR ** Could Not locate host"); return; } } // Устанавливаем порт сервера saServerAddress.sin_port = htons(iPort); // Пытаемся подключиться к серверу iStatus = connect(g_skClientSocket, (struct sockaddr*)&saServerAddress, sizeof(sockaddr)); // Проверяем наличие ошибок if(iStatus == SOCKET_ERROR) { vShowText("** ERROR ** Could Not Connect To Server"); return; } // Убираем кнопки DestroyWindow(hBU_Connect); DestroyWindow(hBU_Host); vShowText("<- Connected To Server ->"); // Устанавливаем флаг подключения g_bConnected = 1; // Устанавливаем флаг, указывающий что право хода принадлежит нам g_bMyTurn = 1; // Отображаем кнопку Turn Done hBU_TurnDone = CreateWindow( "BUTTON", "Turn Done", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 5, 280, 100, 28, g_hWnd, (HMENU)IDC_hBU_TurnDone, g_hInst, NULL); vShowText(":Server waiting, make your turn");
Код подключения очень похож на тот, который я показывал вам в примере подключения к веб-серверу. Клиент сперва получает адрес сервера, а затем пытается установить соединение с ним. Как только соединение успешно установлено, клиент убирает кнопки Connect и Host и отображает новую кнопку Turn Done. В этот момент клиенту дается время, чтобы он мог сделать свой ход. Ход выполнения функции показан на рис. 14.17.
Рис. 14.17. Ход выполнения функции vConnect()
Функция завершения хода выполняет две различных задачи. Если сейчас ваш ход, она отправляет сообщение о завершении хода другому игроку и ждет получения сообщения. Если сейчас ход другого игрока, функция ждет, пока он не завершит свой ход. Ход выполнения функции показан на рис. 14.18.
Рис. 14.18. Ход выполнения функции vTurnDone()
Хотя блок-схема и выглядит запутанной, сам код не слишком сложен. Большая его часть занимается обработкой сообщений от окна. Если ее отбросить, оставшийся код будет выглядеть примерно так:
// Если соединение установлено, проверяем // надо получать или отправлять сообщение о ходе if(g_bConnected) { // Мой ход, отправляю сообщение if(g_bMyTurn) { // Убираем кнопку завершения хода DestroyWindow(hBU_TurnDone); // Отправляем сообщение о завершении хода vSendTurnMessage(); // Ждем получения сообщения vReceiveTurnMessage(); } else { // Ждем получения сообщения } }
Если вы сравните приведенный выше код с тем, который находится в файле main.cpp, то увидите что здесь код значительно короче. Я удалил из него текстовые сообщения, чтобы вам проще было увидеть, что происходит.
Когда приходит время отправлять сообщение о завершении хода, вызывается функция отправки сообщений. Ее код выглядит так:
void vSendTurnMessage(void) { char szTurnPacket[32]; intiBytes = 0; // Создаем пакет-заглушку sprintf(szTurnPacket, "turnpacket"); // Отправляем пакет iBytes = send(g_skClientSocket, szTurnPacket, 32, 0); if(iBytes != SOCKET_ERROR) { } else { vShowText("** ERROR ** Sending"); return; } // Устанавливаем режим приема g_bMyTurn = 0; }
Код начинается с создания пакета для отправки его клиенту или серверу. Для демонстрационных целей в пакет помещается строка текста. Как только пакет собран, код посылает его другому игроку. Программа блокируется и ждет, пока получатель не подтвердит, что данные приняты. Так только подтверждение получено, сбрасывается флаг хода и функция завершает работу.
Когда вы ждете получения хода от другого игрока, работает функция приема сообщения о ходе. Она сидит и ждет пока пакет с данными хода не придет по проводам. Как только прибывает пакет, устанавливается флаг хода и отображается кнопка Turn Done. Вот как выглядит код, выполняющий эти задачи:
void vReceiveTurnMessage(void) { char szTurnPacket[32]; intiBytes = 0; iBytes = recv(g_skClientSocket, szTurnPacket, 32, 0); // Проверка возвращенного кода if(iBytes != SOCKET_ERROR) { } else { vShowText("** ERROR ** Receiving"); return; } // Переключение в режим отправки g_bMyTurn = 1; // Отображение кнопки Turn Done hBU_TurnDone = CreateWindow( "BUTTON", "Turn Done", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 5, 280, 100, 28, g_hWnd, (HMENU)IDC_hBU_TurnDone, g_hInst, NULL); }
В функции приема я вызываю функцию recv для приема пакета от другого игрока. Как только пакет пришел, код устанавливает флаг хода и создает кнопку Turn Done. Вот и все об отправке и получении пакетов!
В этом разделе я только прикоснулся к поверхности огромной темы программирования многопользовательских игр. Надеюсь, вам хватит предоставленной информации, чтобы хотя бы начать работу над походовой сетевой игрой.
netlib.narod.ru | < Назад | Оглавление | Далее > |