C сетевое программирование примеры. Отличный гайд по сетевому программированию

Мне очень нравится весь цикл статей, плюс всегда хотелось попробовать себя в качестве переводчика. Возможно, опытным разработчикам статья покажется слишком очевидной, но, как мне кажется, польза от нее в любом случае будет.
Первая статья -

Прием и передача пакетов данных

Введение
Привет, меня зовут Гленн Фидлер и я приветствую вас в своей второй статье из цикла “Сетевое программирование для разработчиков игр”.

В мы обсудили различные способы передачи данных между компьютерами по сети, и в конце решили использовать протокол UDP, а не TCP. UDP мы решили использовать для того, чтобы иметь возможность пересылать данные без задержек, связанных с ожиданием повторной пересылки пакетов.

А сейчас я собираюсь рассказать вам, как на практике использовать UDP для отправки и приема пакетов.

BSD сокеты
В большинстве современных ОС имеется какая-нибудь реализация сокетов, основанная на BSD сокетах (сокетах Беркли).

Сокеты BSD оперируют простыми функциями, такими, как “socket”, “bind”, “sendto” и “recvfrom”. Конечно, вы можете обращаться к этим функциями напрямую, но в таком случае ваш код будет зависим от платформы, так как их реализации в разных ОС могут немного отличаться.

Поэтому, хоть я далее и приведу первый простой пример взаимодействия с BSD сокетами, в дальнейшем мы не будем использовать их напрямую. Вместо этого, после освоения базового функционала, мы напишем несколько классов, которые абстрагируют всю работу с сокетами, чтобы в дальнейшем наш код был платформонезависимым.

Особенности разных ОС
Для начала напишем код, который будет определять текущую ОС, чтобы мы могли учесть различия в работе сокетов:

// platform detection #define PLATFORM_WINDOWS 1 #define PLATFORM_MAC 2 #define PLATFORM_UNIX 3 #if defined(_WIN32) #define PLATFORM PLATFORM_WINDOWS #elif defined(__APPLE__) #define PLATFORM PLATFORM_MAC #else #define PLATFORM PLATFORM_UNIX #endif
Теперь подключим заголовочные файлы, нужные для работы с сокетами. Так как набор необходимых заголовочных файлов зависит от текущей ОС, здесь мы используем код #define, написанный выше, чтобы определить, какие файлы нужно подключать.

#if PLATFORM == PLATFORM_WINDOWS #include #elif PLATFORM == PLATFORM_MAC || PLATFORM == PLATFORM_UNIX #include #include #include #endif
В UNIX системах функции работы с сокетами входят в стандартные системные библиотеки, поэтому никакие сторонние библиотеки нам в этом случае не нужны. Однако в Windows для этих целей нам нужно подключить библиотеку winsock.

Вот небольшая хитрость, как можно это сделать без изменения проекта или makefile’а:

#if PLATFORM == PLATFORM_WINDOWS #pragma comment(lib, "wsock32.lib") #endif
Мне нравится этот прием потому, что я ленивый. Вы, конечно, можете подключить библиотеку в проект или в makefile.

Инициализация сокетов
В большинстве unix-like операционных систем (включая macosx) не требуется никаких особых действий для инициализации функционала работы с сокетами, но в Windows нужно сначала сделать пару па - нужно вызвать функцию “WSAStartup” перед использованием любых функций работы с сокетами, а после окончания работы - вызвать “WSACleanup”.

Давайте добавим две новые функции:

Inline bool InitializeSockets() { #if PLATFORM == PLATFORM_WINDOWS WSADATA WsaData; return WSAStartup(MAKEWORD(2,2), &WsaData) == NO_ERROR; #else return true; #endif } inline void ShutdownSockets() { #if PLATFORM == PLATFORM_WINDOWS WSACleanup(); #endif }
Теперь мы имеем независимый от платформы код инициализации и завершения работы с сокетами. На платформах, которые не требуют инициализации, данный код просто не делает ничего.

Создаем сокет
Теперь мы можем создать UDP сокет. Это делается так:

Int handle = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (handle <= 0) { printf("failed to create socket\n"); return false; }
Далее мы должны привязать сокет к определенному номеру порта (к примеру, 30000). У каждого сокета должен быть свой уникальный порт, так как, когда приходит новый пакет, номер порта определяет, какому сокету его передать. Не используйте номера портов меньшие, чем 1024 - они зарезервированы системой.

Если вам все равно, какой номер порта использовать для сокета, вы можете просто передать в функцию “0”, и тогда система сама выделит вам какой-нибудь незанятый порт.

Sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons((unsigned short) port); if (bind(handle, (const sockaddr*) &address, sizeof(sockaddr_in)) < 0) { printf("failed to bind socket\n"); return false; }
Теперь наш сокет готов для передачи и приема пакетов данных.

Но что это за таинственная функция “htons” вызывается в коде? Это просто небольшая вспомогательная функция, которая переводит порядок следования байтов в 16-битном целом числе - из текущего (little- или big-endian) в big-endian, который используется при сетевом взаимодействии. Ее нужно вызывать каждый раз, когда вы используете целые числа при работе с сокетами напрямую.

Вы встретите функцию “htons” и ее 32-битного двойника - “htonl” в этой статье еще несколько раз, так что будьте внимательны.

Перевод сокета в неблокирующий режим
По умолчанию сокеты находится в так называемом “блокирующем режиме”. Это означает, что если вы попытаетесь прочитать из него данные с помощью “recvfrom”, функция не вернет значение, пока не сокет не получит пакет с данными, которые можно прочитать. Такое поведение нам совсем не подходит. Игры - это приложения, работающие в реальном времени, со скоростью от 30 до 60 кадров в секунду, и игра не может просто остановиться и ждать, пока не придет пакет с данными!

Решить эту проблему можно переведя сокет в “неблокирующий режим” после его создания. В этом режиме функция “recvfrom”, если отсутствуют данные для чтения из сокета, сразу возвращает определенное значение, показывающее, что нужно будет вызвать ее еще раз, когда в сокете появятся данные.

Перевести сокет в неблокирующий режим можно следующим образом:

#if PLATFORM == PLATFORM_MAC || PLATFORM == PLATFORM_UNIX int nonBlocking = 1; if (fcntl(handle, F_SETFL, O_NONBLOCK, nonBlocking) == -1) { printf("failed to set non-blocking socket\n"); return false; } #elif PLATFORM == PLATFORM_WINDOWS DWORD nonBlocking = 1; if (ioctlsocket(handle, FIONBIO, &nonBlocking) != 0) { printf("failed to set non-blocking socket\n"); return false; } #endif
Как вы можете видеть, в Windows нет функции “fcntl”, поэтому вместе нее мы используем “ioctlsocket”.

Отправка пакетов
UDP - это протокол без поддержки соединений, поэтому при каждой отправке пакета нам нужно указывать адрес получателя. Можно использовать один и тот же UDP сокет для отправки пакетов на разные IP адреса - на другом конце сокета не обязательно должен быть один компьютер.

Переслать пакет на определенный адрес можно следующим образом:

Int sent_bytes = sendto(handle, (const char*)packet_data, packet_size, 0, (sockaddr*)&address, sizeof(sockaddr_in)); if (sent_bytes != packet_size) { printf("failed to send packet: return value = %d\n", sent_bytes); return false; }
Обратите внимание - возвращаемое функцией “sendto” значение показывает только, был ли пакет успешно отправлен с локального компьютера. Но оно не показывает, был ли пакет принят адресатом! В UDP нет средств для определения, дошел ли пакет по назначению или нет.

В коде, приведенном выше, мы передаем структуру “sockaddr_in” в качестве адреса назначения. Как нам получить эту структуру?

Допустим, мы хотим отправить пакет по адресу 207.45.186.98:30000.

Запишем адрес в следующей форме:

Unsigned int a = 207; unsigned int b = 45; unsigned int c = 186; unsigned int d = 98; unsigned short port = 30000;
И нужно сделать еще пару преобразований, чтобы привести его к форме, которую понимает “sendto”:

Unsigned int destination_address = (a << 24) | (b << 16) | (c << 8) | d; unsigned short destination_port = port; sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(destination_address); address.sin_port = htons(destination_port);
Как видно, сначала мы объединяем числа a, b, c, d (которые лежат в диапазоне ) в одно целое число, в котором каждый байт - это одно из исходных чисел. Затем мы инициализируем структуру “sockaddr_in” нашими адресом назначения и портом, при этом не забыв конвертировать порядок байтов с помощью функций “htonl” и “htons”.

Отдельно стоит выделить случай, когда нужно передать пакет самому себе: при этом не нужно выяснять IP адрес локальной машины, а можно просто использовать 127.0.0.1 в качестве адреса (адрес локальной петли), и пакет будет отправлен на локальный компьютер.

Прием пакетов
После того, как мы привязали UDP сокет к порту, все UDP пакеты, приходящие на IP адрес и порт нашего сокета, будут ставиться в очередь. Поэтому для приема пакетов мы просто в цикле вызываем “recvfrom”, пока он не выдаст ошибку, означающую, что пакетов для чтения в очерели не осталось.

Так как протокол UDP не поддерживает соединения, пакеты могут приходить с множества различных компьютеров сети. Каждый раз, когда мы принимаем пакет, функция “recvfrom” выдает нам IP адрес и порт отправителя, и поэтому мы знаем, кто отправил этот пакет.

Код приема пакетов в цикле:

While (true) { unsigned char packet_data; unsigned int maximum_packet_size = sizeof(packet_data); #if PLATFORM == PLATFORM_WINDOWS typedef int socklen_t; #endif sockaddr_in from; socklen_t fromLength = sizeof(from); int received_bytes = recvfrom(socket, (char*)packet_data, maximum_packet_size, 0, (sockaddr*)&from, &fromLength); if (received_bytes <= 0) break; unsigned int from_address = ntohl(from.sin_addr.s_addr); unsigned int from_port = ntohs(from.sin_port); // process received packet }
Пакеты, размер которых больше, чем размер буфера приема, будут просто втихую удалены из очереди. Так что, если вы используете буфер размером 256 байтов, как в примере выше, и кто-то присылает вам пакет в 300 байт, он будет отброшен. Вы не получите просто первые 256 байтов из пакета.

Но, поскольку мы пишем свой собственный протокол, для нас это не станет проблемой. Просто всегда будьте внимательны и проверяете, чтобы размер буфера приема был достаточно большим, и мог вместить самый большой пакет, который вам могут прислать.

Закрытие сокета
На большинстве unix-like систем, сокеты представляют собой файловые дескрипторы, поэтому для того, чтобы закрыть сокеты после использования, можно использовать стандартную функцию “close”. Однако, Windows, как всегда, выделяется, и в ней нам нужно использовать “closesocket”.

#if PLATFORM == PLATFORM_MAC || PLATFORM == PLATFORM_UNIX close(socket); #elif PLATFORM == PLATFORM_WINDOWS closesocket(socket); #endif
Так держать, Windows!

Класс сокета
Итак, мы разобрались со всеми основными операциями: создание сокета, привязка его к порту, перевод в неблокирующий режим, отправка и прием пакетов, и, в конце, закрытие сокета.

Но, как вы могли заметить, все эти операции немного отличаются от платформы к платформе, и, конечно, трудно каждый раз при работе с сокетами вспоминать особенности разных платформ и писать все эти #ifdef.

Поэтому мы сделаем класс-обертку “Socket” для всех этих операций. Также мы создадим класс “Address”, чтобы было проще работать с IP адресами. Он позволит не проводить все манипуляции с “sockaddr_in” каждый раз, когда мы захотим отправить или принять пакет.

Итак, наш класс Socket:

Class Socket { public: Socket(); ~Socket(); bool Open(unsigned short port); void Close(); bool IsOpen() const; bool Send(const Address & destination, const void * data, int size); int Receive(Address & sender, void * data, int size); private: int handle; };
И класс Address:

Class Address { public: Address(); Address(unsigned char a, unsigned char b, unsigned char c, unsigned char d, unsigned short port); Address(unsigned int address, unsigned short port); unsigned int GetAddress() const; unsigned char GetA() const; unsigned char GetB() const; unsigned char GetC() const; unsigned char GetD() const; unsigned short GetPort() const; bool operator == (const Address & other) const; bool operator != (const Address & other) const; private: unsigned int address; unsigned short port; };
Использовать их для приема и передачи нужно следующим образом:

// create socket const int port = 30000; Socket socket; if (!socket.Open(port)) { printf("failed to create socket!\n"); return false; } // send a packet const char data = "hello world!"; socket.Send(Address(127,0,0,1,port), data, sizeof(data)); // receive packets while (true) { Address sender; unsigned char buffer; int bytes_read = socket.Receive(sender, buffer, sizeof(buffer)); if (!bytes_read) break; // process packet }
Как видите, это намного проще, чем работать с BSD сокетами напрямую. И также этот код будет одинаков для всех ОС, потому весь платформозависимый функционал находится внутри классов Socket и Address.

Заключение
Теперь у нас есть независимый от платформы инструмент для отправки и према UDP пакетов.

UDP не поддерживает соединения, и мне хотелось сделать пример, который бы четко это показал. Поэтому я написал небольшую программу , которая считывает список IP адресов из текстового файла и рассылает им пакеты, по одному в секунду. Каждый раз, когда программа принимает пакет, она выводит в консоль адрес и порт компьютера-отправителя и размер принятого пакета.

Вы можете легко настроить программу так, чтобы даже на локальной машине получить несколько узлов, обменивающихся пакетами друг с другом. Для этого просто разным экземплярам программы задайте разные порты, например:

> Node 30000
> Node 30001
> Node 30002
И т.д…

Каждый из узлов будет пересылать пакеты всем остальным узлам, образуя нечто вроде мини peer-to-peer системы.

Я разрабатывал эту программу на MacOSX, но она должна компилироваться на любой unix-like ОС и на Windows, однако если вам для этого потребуется делать какие-либо доработки, сообщите мне.

Теги:

  • udp
  • game development
  • networking
Добавить метки

Введение

Эй! Программирование сокетов тебя достало? Оно слишком заковыристо, чтобы изучить его по манам? Вы хотите писать сетевые программы, но у вас нет времени, чтобы разбираться в дебрях документации, чтобы всего лишь узнать, что перед connect() нужно вызывать bind() и т.д. и т.п.?

И ещё: я наконец нашел время и дополнил гайд информацией о программировании IPv6! Наслаждайтесь!

Аудитория

Этот документ написан в качестве учебника, а не справочного материала. Наверно, лучше всего он будет восприниматься теми, кто только начинает изучать программирование сокетов и ищет точку опоры. Это не полное глобальное руководство по программированию сокетов.
Надеюсь, однако, его будет достаточно, чтобы понять суть и смысл сокетов... :-)

Платформа и компилятор

Официальный сайт и книги

Официальное и изначальное местонахождение этого документа - http://beej.us/guide/bgnet/ . Там вы также найдете примеры кода и переводы руководства на различные языки.

Чтобы купить красиво переплетенные копии этого документа (некоторые называют их "книги"), посетите http://beej.us/guide/url/bgbuy . Я ценю заказы своих книг, поскольку они помогают поддерживать мой документо-писательский образ жизни!

Программистам Solaris и SunOS

При компиляции под Solaris/SunOS вам нужно указать компилятору дополнительные библиотеки для линковки. Для этого просто добавьте в строку компиляции: "-lnsl -lsocket -lresolv" , как-то так:

$ cc - o server server.c - lnsl - lsocket - lresolv

Если всё ещё возникают ошибки, добавьте ещё и "-lxnet". Я не знаю, что это такое, но у некоторых людей это решало проблему.

Другое узкое место - вызов setsockopt(). Прототип отличается от такогого в Linux, так что вместо

int yes= 1 ;


используйте:

char yes= "1" ;

Так как у меня нет соляриса, я не тестировал ничего из этого. Всё это мне пришло в отзывах на e-mail.

Программистам Windows

Исторически этот гайд не рассчитан на windows. Просто потому, что windows я не люблю. Но стоит быть действительно честным и признать, что у windows есть огромная база программ и пользователей, и в сущности это прекрасная ОС.

Я всё ещё надеюсь, что вы попробуете Linux, BSD или любой другой Unix.
Но людям нравится то, что им нравится, и ребята под windows имеют право на свою долю информации. Этот документ полезен и им тоже, с небольшими изменениями в коде.

Во-первых, вы можете поставить такую штуку, как Cygwin. Это коллекция инструментов Unix под Windows. Насколько я понимаю, это позволит оставить код без изменений.

Но некоторые из вас, возможно, захотят писать код под чистый windows. Это очень плохо, и вы должны немедленно поставить Unix!
Нет-нет, шучу. Нужно быть windows-терпеливым ближайшие дни...

Во-первых, вы должны игнорировать почти все хэдер-файлы, которые я использую. Всё, что вам нужно включать, это:

#include

Стоп! Ещё вы должны вызывать WSAStartup() перед всем остальным кодом, относящимся к сокетам. Выглядит это примерно так:

#include

{
WSADATA wsaData; // if this doesn"t work
//WSAData wsaData; // then try this instead

// MAKEWORD(1,1) for Winsock 1.1, MAKEWORD(2,0) for Winsock 2.0:

if (WSAStartup(MAKEWORD(1 , 1 ) , & wsaData) != 0 ) {
fprintf(stderr, "WSAStartup failed.n" ) ;
exit(1 ) ;
}

Также вам нужно прилинковать к проекту библиотеки: обычно это wsock32.lib или winsock32.lib или ws2_32.lib для winsock 2.0. В VC++ это может быть сделано через меню Проект, в меню Настройки... Выберите вкладку компилятор->линковка или что-то вроде того, и добавьте "wsock32.lib" (или какой-то другой похожый.lib).

Ну, я слышал, что это примерно так делается.

В конце работы с сокетами вы должны вызывать WSACleanup().

Если вы сделаете всё это, остальные примеры из этого учебника должны, по идее, работать, хоть и с некоторыми исключениями.
Во-первых, вместо close() вам нужно использовать closesocket(). select() работает только с дескриптором сокета, с дескриптором файла, как в unix, не работает.

Существует так же класс для работы с сокетами, CSocket , можете поискать его в учебниках.

Чтобы получить больше информации о Winsock, читайте Winsock FAQ .

Наконец, насколько я знаю, в windows, к сожалению, нет системы fork(), использующейся в некоторых примерах. Может быть, вы сможете прилинковать для форка библиотеку POSIX, или использовать вместо него CreateProcess(). Форк не принимает аргументов, а CreateProcess принимает миллиарды. Если вам не хочется в них разбираться, используйте CreateThread, это немного проще... К сожалению, дискуссия о многопоточности выходит за рамки этого документа.

1.6 E-Mail политика

Обычно я доступен по электронной почте, если вам нужна помощь или есть вопросы, так чир не стесняйтесь писать, но я не могу гарантировать свой ответ. Я веду довольно занятой жизни и Есть моменты, когда я просто не могу ответить на ваш вопрос. Когда так случается, я обычно просто удаляю сообщение. Ничего личного, у меня просто никогда не будет времени, чтобы дать подробный ответ, которого вы требуете.

Как правило, чем более объемный вопрос, тем менее вероятно, что я отвечу. Если вы можете сузить свой вопрос, прежде чем отправить его мне, и не забудьте включить любую соответствующую информацию (например, платформу, компилятор, сообщения об ошибках, которые вы получаете и все остальное, что, как вам кажется, может мне помочь в устранении неполадок), вы имеете гораздо больше шансов получить ответ.

Если вы не получите ответа, подумайте над ним еще некоторое время, попробуйте найти ответ, и если он по-прежнему неуловимым, то напишите мне снова с дополнительной информацией, которую Вы нашли и, надеюсь, этого мне будет достаточно для помощи вам.

Теперь, когда я загрузил вас мыслями о том, писать мне или не писать, я просто хочу, чтобы вы знали, что я в полной мере оценил все похвалы руководства, которые получил за эти годы. Знание, что книга используется для создания полезных и хороших вещей - реальная поднимает боевой дух, и радует! :-) Спасибо!

1.7 Зеркалирование

Зеркалирование этого материала более чем приветствуется, будь то государственные или частные сайты. Если вы опубликовали зеркало и хотите, чтобы я разместил ссылку на него на главной странице, напишите мне на [email protected] .

Руководство Beej по сетевому программированию © 2009 Brian "Beej Jorgensen" Hall.

За некоторыми исключениями исходного кода и переводов, ниже, эта работа под лицензией Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 License. Чтобы просмотреть копию данной лицензии, посетите http://creativecommons.org/licenses/by-nc-nd/3.0/ или отправьте письмо в Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA.

Одним из конкретных исключений "No Derivative Works" части лицензии является следующее: это руководство может быть свободно переведено на любой язык при условии, что перевод является точным, и руководство перепечатано в полном объеме. Такие же ограничения лицензии относятся как переводу, так и к исходному руководству. Перевод может также включать имя и контактную информацию переводчиков.

Исходный код, представленный в данном документе, настоящим объявляется общественным достоянием, и полностью свободен от каких-либо лицензионных ограничений.

Свяжитесь с [email protected] для получения дополнительной информации.

Среда C++ Builder предоставляет несколько вариантов написания сетевых приложений:

1. Использовать библиотеку winsock.h (библиотека Windows Sockets)
Данный способ хорош тем, что он универсален, и это является безусловным плюсом. Но если вы выбрали данный способ, будьте готовы к тому, что вам придётся копаться в WinAPI и создавать потоки, а это не каждому под силу. Поэтому данный способ не сильно подходит в ситуации, когда времени на написание готовой программы не так уж и много.

2. Использовать компоненты Indy
Если вы будете использовать этот способ, будьте готовы испытать его один серьёзный минус - слабая переносимость. Компоненты очень серьёзно зависят от версии библиотеки, а с каждой новой версии программной среды поставляется новая версия библиотеки Indy, в которой добавляются или перерабатываются новые свойства и методы компонент. И этот способ я тоже быстро отсёк, потому что хотелось найти нечто более универсальное.

3. Использовать компоненты TcpServer и TcpClient
Лично я пытался соорудить чат на этих компонентах, но так ничего и не вышло. Надеюсь, что у вас, уважаемые хабраюзеры, был успешный опыт использования данных компонент.

4. Использовать компоненты ServerSocket и ClientSocket
Фундаментально эти компоненты основаны на Windows Sockets, только здесь вам не придётся возиться с WinAPI. Всё сводится к тому, чтобы грамотно использовать свойства и методы этих компонент. Этот способ экономит достаточно времени, и поэтому для ситуаций, когда нужен результат за короткое время, он подходит идеально.

Итак, пару слов о этих компонентах.
Находятся они на вкладке «Internet» палитры компонент в C++ Builder 6. В поздних версиях данной программной среды разработчики убрали рассматриваемые компоненты с этой палитры, но в стандартной комплектации поставляется пакет установки (в C++ Builder 2010 - файл dclsockets140.bpl: имя файла в разных версиях может отличаться только последней цифрой), который нужно будет установить через настройки проекта (меню «Project» -> «Options» -> «Packages» -> кнопка «Add…»). После установки указанного пакета компоненты появятся на палитре во вкладке «Internet».

ServerSocket и ClientSocket могут работать в двух режимах: блокирующем и асинхронном. Отличия в том, что при блокирующем режиме программа-сервер приостановит свою работу до тех пор, пока не подключится новый клиент, а каждый клиент обслуживается в отдельном потоке. Поэтому оптимальнее выбрать асинхронный режим, в котором программа-сервер будет спокойно продолжать работу, не дожидаясь очередного клиента.

Для того, чтобы создать сервер, в компоненте ServerSocket достаточно лишь указать порт, который будет использоваться. Это целое число от 0 до 65535. После этого используются лишь следующие методы: Open() для создания сервера и Close() для его разрушения.

Используя компоненту ClientSocket, можно создать клиентскую часть приложения. У компоненты ClientSocket имеются следующие свойства:
ClientSocket1->Address; // переменная строкового типа, в которой прописывается IP-адрес сервера
ClientSocket1->Host; // переменная строкового типа, в которой прописывается DNS сервера
ClientSocket1->Port; // переменная целого типа, определяющая порт сервера, к которому производится подключение
Стоит отметить, что свойство Host является более приоритетном, чем свойство Address. Таким образом, если вы укажете оба свойства, то подключение установится с DNS сервера, который указан в свойстве Host. Подключение устанавливается или разрывается с помощью тех же методов Open() и Close().

Общим для обеих компонент является булево свойство Active, по которому определяется корректность (или активность) соединения.

Как видите, всё просто. Но будьте внимательнее с последовательностью событий, совершаемых в данных компонентах, и я верю, что через несколько дней вы сможете написать простенький чат для общения с друзьями.

Результат моего исследования этой темы вылился в




Предисловие

"Вот так... Копишь миллионы, копишь...
А потом БАЦ! ... тортом тебе в морду!"

Очень-очень известный и всеми любимый герой IT

Когда я впервые столкнулся с необходимостью написать приложение, которое могло бы взаимодействовать с таким же приложением, запущенным на другом компьютере, я был неприятно удивлен дефицитом полезной русскоязычной документации по этому делу. Конечно, я знал английский, и для меня не составило особого труда разобраться в тонкостях сетевого программирования, но что делать человеку который не знает ничего кроме русского ("русский язык велик и могук!" :))? Ответа нет... И даже если он знает английский язык, то поначалу это ему не очень-то поможет. Лично я не видел еще ни одного систематизированного и каталогизированного источника информации о программировании сетевых приложений, который бы в той или иной степени охватывал весь этот огромный хаос.

Итак, уже сделано все, что касается однопользовательских режимов игры, однако было бы неплохо добавить возможность игры по сети. И ты, конечно, даже и не представляешь, с чего начать...... Хорошо, я тебе помогу, вернее, тебе поможет мой CGNP. Для того чтобы не было недоразумений, я сразу оговорюсь, что написанное ниже рассчитано на тех, кто кодит на с/с++ (MSVC++ в Windows-системах и gсс/g++ в никсах). Я также предполагаю, что у читателей есть хотя бы минимальный набор знаний об устройстве и функционировании компьютерных сетей. Необязателен, но желателен справочник по Windows API 32 под рукой или доступ к MSDN (юниксоидам в этом плане повезло - man pages не могут быть "не под рукой" ;)). Еще я хотел бы сделать предупреждение: представленный ниже материал не претендует на полноту освещения затронутых в нем тем, а также на абсолютную точность.

И наконец, перед тем, как мы окунемся в омут с головой, я дам еще один совет: дружище, выучи все-таки английский! Он тебе очень пригодится. Ведь когда ты захочешь стать гуру сетевого программирования, тебе придется прочесть очень много RFC-документов, а ошибки перевода и неправильного толкования технических спецификаций являются "бомбами замедленного действия"!

Модель OSI

Чтобы понять все принципы взаимодействия компьютеров на расстоянии, надо знать так называемую модель OSI (ISO OSI == International Organization for Standardization Open System Interconnection - Взаимодействие Открытых Систем по Стандарту Международной Организации по Стандартизации)... Теперь можем сделать перерыв, чтобы ты, уважаемый читатель, смог еще пять раз перечитать предыдущее предложение и понять его смысл, после чего мы разберемся, что такое OSI, и с чем ее едят...

Итак, модель OSI определяет несколько "уровней" взаимодействия компьютеров на расстоянии (я намеренно избегаю словосочетания "по сети", и ты скоро поймешь почему). Вот эти уровни:

7. Прикладной

Это уровень, максимально приближенный к пользовательскому интерфейсу. Пользователи конечного программно продукта не волнует, как передаются данные, зачем и через какое место... Он сказали "ХОЧУ!" - а мы, программисты, должны им это обеспечить. В качестве примера можно взять на рассмотрение любую сетевую игру: для игрока она работает на этом уровне. Пользователь куда то ткнул, в интерфейсной части программы зафиксирована его команда. Что надо передать? Что то приняли, что произошло в мире игры?

6. Представительский

Здесь программист имеет дело с данными, полученными от низших уровней. В основном, это конвертирование и представление данных в удобоваримом для пользователя виде.

5. Сеансовый

Этот уровень позволяет пользователям осуществлять "сеансы связи". То есть именно на этом уровне передача пакетов становится для программиста прозрачной, и он может, не задумываясь о реализации, непосредственно передавать данные, как цельный поток. Здесь на сцену вступают протоколы HTTP, FTP, Telnet, SMTP и т.д.

4. Транспортный

Осуществляет контроль над передачей данных (сетевых пакетов). То есть, проверяет их целостность при передаче, распределяет нагрузку и т.д. Этот уровень реализует такие протоколы, как TCP, UDP и т.д. Для нас представляет наибольший интерес.

3. Сетевой

Логически контролирует адресацию в сети, маршрутизацию и т.д. Должен быть интересен разработчикам новых протоколов и стандартов. На этом уровне реализованы протоколы IP, IPX, IGMP, ICMP, ARP. В основном, управляется драйверами и операционными системами. Сюда влезать, конечно, стоит, но только когда ты знаешь, что делаешь, и полностью в себе уверен.

2. Канальный

Этот уровень определяет, как биты собираются в пакеты, какую служебную информацию надо передавать и как на неё реагировать, но поле данных каждого пакета максимально сырое и представляет из себя просто биты в навал, нет вообще ни какой последовательности пакетов в длинных информационных потоках. В поле данных просто вложены пакеты сетевых протоколов, всё, что чего не хватает должно быть сделано в них, или ещё выше.

1. Аппаратный (Физический)

Контролирует передачи представление битов и служебных сигналов физическими процессами и отправку физических сигналов между аппаратными устройствами, входящими в сеть. То есть управляет передачей электронов по проводам. Нас он не интересует, потому что все, что находится на этом уровне, контролируется аппаратными средствами (реализация этого уровня - это задача производителей хабов, мультиплексоров, повторителей и другого оборудования). Мы не физики-радиолюбители, а геймдевелоперы.

Итак, подведем небольшой итог к тому, что было представлено... Мы видим, что, чем выше уровень - тем выше степень абстракции от передачи данных, к работе с самими данными. Это и есть смысл всей модели OSI: поднимаясь все выше и выше по ступенькам ее лестницы, мы все меньше и меньше заботимся о том, как данные передаются, мы все больше и больше становимся заинтересованными в самих данных, нежели в средствах для их передачи. Каждый следующий уровень скрывает в себе предыдущий, облегчая жизнь пользователю этого уровня, будь он программист, радиоинженер или твоя подруга, которая не знает, как настроить MS Outlook Express...

Нас, как программистов, интересуют уровни 3, 4 и 5. Мы должны использовать средства, которые они предоставляют, для того чтобы построить 6 и 7 уровни, с которыми смогут работать конечные пользователи.

Начнем потихоньку... :)

Сокеты и бла-бла-бла...

У каждой уважающей себя современной операционной системы есть средства для взаимодействия с другими компьютерами. Самым распространенным среди программистов средством для упомянутых целей являются сокеты. Сокеты - это API (Application Programming Interface - Интерфейс Программирования Приложений) для работы с уровнями OSI. Сокеты настолько гибки, что позволяют работать почти с любым из уровней модели OSI. Хочешь - формируй IP-пакеты руками и займись хакингом, отправляя "неправильные" пакеты, которые будут вводить сервера в ступор, хочешь - займись более благоразумным делом и создай новый удобный голосовой чат, хочешь - игрульку по сети гоняй, не хочешь - твое право, но этот случай мы в данном руководстве не рассматриваем... :)

Когда мы создаем сокет (socket - гнездо), мы получаем возможность доступа к нужному нам уровню OSI. Ну а дальше мы можем использовать соответствующие вызовы для взаимодействия с ним. Для того чтобы понять сокеты, можно провести аналогию с телефонным аппаратом и телефонной трубкой. Сокеты устроены таким образом, что они могут взаимодействовать с ОС на любом уровне OSI, скрывая ту часть реализации, которой мы не интересуемся (тебя же не волнует, как работает телефон, когда ты набираешь 03). Телефоны и сокеты бывают разные: бывают старые телефоны с дисковым набором и бывают низкоуровневые сокеты для работы с Ethernet-фреймами, бывают супер-модные цифровые телефоны и бывают сокеты для работы с верхними уровнями стека протоколов... и т.д. Причем вызовы для всех типов сокетов одни и те же, что, имхо, очень удобно. Когда мы создаем сокет, мы также заставляем систему организовать два канала: входящий (это как громкоговоритель у телефона) и исходящий (микрофон). Осуществляя чтение и запись в эти каналы, мы приказываем системе взять на себя дальнейшую судьбу данных, т.е. передать и проследить, чтоб данные дошли вовремя, в нужной последовательности, не искаженные и т.п. Система должна давать (и дает) максимум гарантий (для каждого уровня OSI - гарантии свои), что данные будут переданы правильно. Наша задача - поместить их в очередь, а на другом конце - прочитать из входящей очереди и обработать должным образом. Все остальное - нам ни к чему. Еще один плюс - сокеты переносимы. То есть изначально концепция сокетов была разработана в Berkeley, поэтому классическая реализация сокетов называется Berkeley sockets или BSD sockets (BSD == Berkeley Software Distribution). В дальнейшем, почти все ОС тем или иным образом унаследовали эту реализацию. В каждой ОС степень поддержки сокетов разная, но точно могу сказать: в современных операционных системах MS и *nix - сокеты поддерживаются настолько, насколько нам, геймдевелоперам, они могут понадобиться. Больше нам и не нужно, потому что мы не кодим под экзотические ОС, потому что, в свою очередь, геймеры (они наша целевая аудитория) на таковых не сидят. Однако по мере изучения мы будем придерживаться классической реализации BSD sockets, и стараться по минимуму использовать системно-зависимый код.