Пример работы с сокетами для window


Содержание

Введение в веб-сокеты

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

Краткая история веб-приложений реального времени

Интернет был построен на представлении о том, что забота браузера– запрос данных с сервера, а забота сервера – обслуживание этих запросов. Эта парадигма не подвергалась сомнению несколько лет. Но с появлением AJAX в 2005 году многие начали работать над созданием двунаправленных соединений.

Веб-приложения значительно увеличивались в размере. Сдерживающим фактором для их роста была традиционная модель HTTP. Чтобы преодолеть это, были созданы несколько стратегий, позволяющих серверам «проталкивать» (push) данные клиенту. Одной из наиболее популярных стала стратегия «длинного опроса» . Она подразумевает поддержание HTTP- соединения открытым до тех пор,пока у сервера есть данные для отправки клиенту.

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

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

Как работают веб-сокеты

Веб-сокет создает постоянное соединение между клиентом и сервером, которое обе стороны могут использовать для отправки данных.

Браузер устанавливает соединение по веб-сокету при помощи «рукопожатия». Этот процесс начинается с отправки клиентом обычного запроса HTTP на сервер. В этот запрос включается заголовок Upgrade, который сообщает серверу, что браузер хочет установить соединение по веб-сокету.

Вот упрощённый пример первоначальных заголовков запроса.

GET ws ://websocket.example.com/ HTTP/1.1

Замечание : URL-адреса веб-сокетов используют протокол ws . Также существует протокол wss для безопасных соединений, который является эквивалентом HTTPS.

Если сервер поддерживает протокол WebSocket, он сообщает об этом с помощью заголовка Upgrade в ответе.

HTTP /1.1 101 WebSocket Protocol Handshake

Date : Wed, 16 Oct 2013 10:07:34 GMT

После того, как рукопожатие выполнено, первоначальное соединение HTTP заменяется соединением по веб-сокету, которое использует то же соединение TCP/IP . На этом этапе любая из сторон может начать отправку данных.

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

Чтобы убедиться, что сообщение будет правильно интерпретировано на стороне клиента, каждому фрагменту предшествуют от 4 до 12 байт данных о полезной нагрузке. Использование обмена сообщениями на основе фрагментов позволяет снизить объем дополнительных данных, что приводит к сокращению задержек.

Замечание : Стоит отметить, что клиент будет уведомлен о новом сообщении только, когда сервер передаст все его фрагменты.

Создаём демо-пример

Создание приложения на основе веб-сокетов

Мы создадим простое приложение, которое соединяется с сервером по веб-сокету. Перед тем, как мы углубимся в детали API, нужно создать несколько файлов.

Посмотреть на CodePen

Создайте файл index.html и добавьте в него следующую разметку.

Файл style.css , на который ссылается этот код, находится в архиве для загрузки. Далее создадим файл app.js и добавим в него следующий код.

Мы создали несколько переменных и инициализировали их ссылками на ключевые элементы страницы.

Открытие соединений

Теперь, когда готов костяк приложения, можно начать изучать WebSocket API. Для начала узнаем, как создать новое соединение WebSocket. Для этого нужно вызвать конструктор класса WebSocket и передать ему URL сервера.

Скопируйте следующий код в файл app.js , чтобы создать новое соединение.

После того, как соединение установлено, возникнет событие open объекта WebSocket. Добавим обработчик события, который обновит статус элемента

Добавьте следующий код в файл app.js .

Также мы добавляем класс open элементу

Обработка ошибок

Обработка ошибок осуществляется через событие error . Добавьте следующий код, который будет записывать ошибки в консоль.

Отправка сообщений

Чтобы отправить сообщение по веб-сокет, нужно вызвать метод send() объекта WebSocket, передав ему данные для отправки.

Можно отправлять как текст, так и двоичные данные. В нашем приложении нужно передавать содержимое текстового поля на сервер при отправке формы. Чтобы сделать это, надо определить обработчик события отправки формы.

Добавьте следующий код в файл app.js .

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

Получение сообщений

При получении сообщения вызывается событие message . Оно включает в себя свойство data , которое можно использовать для доступа к содержимому сообщения.

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

Чтобы добиться этого, скопируйте следующий код в файл app.js .

Закрытие соединений

Когда вы закончите работу с веб-сокетом, нужно разорвать соединение, используя метод close() .

После того, как соединение будет разорвано, браузер вызовет событие close . Добавление обработчика события close позволит выполнить любую «уборку», которая потребуется.

Теперь нам нужно обновить статус соединения при его разрыве. Добавьте следующий код в файл app.js :

Чтобы завершить приложение, нужно добавить обработчик события, который будет вызываться при нажатии кнопки «Close Connection». Он должен вызывать метод close() объекта WebSocket.

Наше приложение готово!

Откройте файл index.html в браузере и попробуйте отправить несколько сообщений. Вы увидите, что сервер отправляет сообщения обратно.

Мониторинг трафика веб-сокета с помощью инструментов для разработчиков в Chrome

«Инструменты разработчика», доступные в браузере Google Chrome включают в себя средства для мониторинга трафика. Чтобы использовать этот инструмент:

  • Откройте «Инструменты разработчика».
  • Перейдите на вкладку Network.
  • Кликните по записи, соответствующей вашему соединению по веб-сокету.
  • Перейдите на вкладку Frames.

Эти инструменты предоставляют общую информацию о данных, переданных через соединение.

WebSocket на сервере

В этой статье мы сфокусировали внимание на том, как использовать веб-сокеты на стороне клиента. Если вы хотите создать собственный сервер WebSocket, существует множество библиотек, которые могут в этом помочь. Одна из наиболее популярных – socket.io , библиотека Node.JS.

  • C++: libwebsockets ;
  • Erlang: Shirasu.ws ;
  • Java: Jetty ;
  • Node.JS: ws ;
  • Ruby: em-websocket ;
  • Python: Tornado , pywebsocket ;
  • PHP: Ratchet , phpws .

Поддержка браузерами

Веб-сокеты поддерживаются практически во всех современных браузерах. Единственными исключениями являются Android- браузеры и Opera Mini.

Заключительные мысли

В этой статье вы узнали о протоколе WebSocket. А также том, как использовать его API для создания веб-приложений, работающих в режиме реального времени.

WebSocket – это большой шаг в эволюции интернета. Возможность создания соединения с низкими затратами ресурсов открывает путь к новому поколению веб-приложений.

Данная публикация представляет собой перевод статьи « An Introduction to WebSockets » , подготовленной дружной командой проекта Интернет-технологии.ру

Веб-сервер на C++ и сокетах

Создадим HTTP-сервер, который обрабатывает запросы браузера и возвращает ответ в виде HTML-страницы.

Введение в HTTP

Для начала разберемся, что из себя представляет HTTP. Это текстовый протокол для обмена данными между браузером и веб-сервером.

Пример HTTP-запроса:

Первая строка передает метод запроса, идентификатор ресурса (URI) и версию HTTP-протокола. Затем перечисляются заголовки запроса, в которых браузер передает имя хоста, поддерживаемые кодировки, cookie и другие служебные параметры. После каждого заголовка ставится символ переноса строки \r\n .

У некоторых запросов есть тело. Когда отправляется форма методом POST, в теле запроса передаются значения полей этой формы.

Тело запроса отделяется от заголовков одной пустой строкой. Заголовок «Content-Type» говорит серверу, в каком формате закодировано тело запроса. По умолчанию, в HTML-форме данные кодируются методом «application/x-www-form-urlencoded».

Иногда необходимо передать данные в другом формате. Например, при загрузке файлов на сервер, бинарные данные кодируются методом «multipart/form-data».

Сервер обрабатывает запрос клиента и возвращает ответ.

Пример ответа сервера:

В первой строке ответа передается версия протокола и статус ответа. Для успешных запросов обычно используется статус «200 OK». Если ресурс не найден на сервере, возвращается «404 Not Found».

Тело ответа так же, как и у запроса, отделяется от заголовков одной пустой строкой.

Полная спецификации протокола HTTP описывается в стандарте rfc-2068. По понятным причинам, мы не будем реализовывать все возможности протокола в рамках этого материала. Достаточно реализовать поддержку работы с заголовками запроса и ответа, получение метода запроса, версии протокола и URL-адреса.

Что будет делать сервер?

Сервер будет принимать запросы клиентов, парсить заголовки и тело запроса, и возвращать тестовую HTML-страничку, на которой отображены данные запроса клиента (запрошенный URL, метод запроса, cookie и другие заголовки).

О сокетах

Для работы с сетью на низком уровне традиционно используют сокеты. Сокет — это абстракция, которая позволяет работать с сетевыми ресурсами, как с файлами. Мы можем писать и читать данные из сокета почти так же, как из обычного файла.

В этом материале мы будем работать с виндовой реализацией сокетов, которая находится в заголовочном файле . В Unix-подобных ОС принцип работы с сокетами такой же, только отличается API. Вы можете подробнее почитать о сокетах Беркли, которые используются в GNU/Linux.

Создание сокета

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

Мы подготовили все данные, которые необходимо для создания сокета и создали сам сокет. Функция socket возвращает целочисленное значение файлового дескриптора, который выделен операционной системой под сокет.

Привязка сокета к адресу (bind)

Следующим шагом, нам необходимо привязать IP-адрес к сокету, чтобы он мог принимать входящие соединения. Для привязки конкретного адреса к сокету используется фукнция bind . Она принимает целочисленный идентификатор файлового дескриптора сокета, адрес (поле ai_addr из структуры addrinfo ) и размер адреса в байтах (используется для поддержки IPv6).

Подготовка сокета к принятию входящих соединений (listen)

Подготовим сокет к принятию входящих соединений от клиентов. Это делается с помощью функции listen . Она принимает дескриптор слушающего сокета и максимальное количество одновременных соединений.

В случае ошибки, функция listen возращает значение константы SOCKET_ERROR . При успешном выполнении она вернет 0.

В константе SOMAXCONN хранится максимально возможное число одновременных TCP-соединений. Это ограничение работает на уровне ядра ОС.

Ожидание входящего соединения (accept)

Функция accept ожидает запрос на установку TCP-соединения от удаленного хоста. В качестве аргумента ей передается дескриптор слушающего сокета.

При успешной установке TCP-соединения, для него создается новый сокет. Функция accept возвращает дескриптор этого сокета. Если произошла ошибка соединения, то возвращается значение INVALID_SOCKET .

Получение запроса и отправка ответа

После установки соединение с сервером, браузер отправляет HTTP-запрос. Мы получаем содержимое запроса через функцию recv . Она принимает дескриптор TCP-соединения (в нашем случае это client_socket ), указатель на буфер для сохранения полученных данных, размер буфера в байтах и дополнительные флаги (которые сейчас нас не интересуют).

При успешном выполнении функция recv вернет размер полученных данных. В случае ошибки возвращается значение SOCKET_ERROR . Если соединение было закрыто клиентом, то возвращается 0.

Мы создадим буфер размером 1024 байта для сохранения HTTP-запроса.

После получения запроса мы сразу же отправили ответ клиенту с помощью функции send . Она принимает дескриптор сокета, строку с данными ответа и размер ответа в байтах.

В случае ошибки, функция возвращает значение SOCKET_ERROR . В случае успеха — количество переданных байт.

Попробуем скомпилировать программу, не забыв предварительно завершить функцию main .

Если скомпилировать и запустить программу, то окно консоли «подвиснет» в ожидании запроса на установление TCP-соединения. Откройте в браузере адрес http://127.0.0.1:8000/. Сервер вернет ответ, как на рисунке ниже и завершит работу.

Последовательная обработка запросов

Чтобы сервер не завершал работу после обработки первого запроса, а продолжал обрабатывать новые соединения, нужно зациклить ту часть кода, которая принимает запрос на установку соединения и возвращает ответ.

Когда сервер закончит обработку запроса одного клиента, он закроет соединение с ним и будет ожидать нового запроса.

Во второй части этой статьи мы напишем парсер HTTP-заголовков и создадим нормальное API для управления HTTP-запросами и ответами.

Примечание: если вы используете MinGW в Windows, то библиотеку Ws2_32.lib нужно вручную прописать в настройках линковщика.

Записки программиста

Асинхронная работа с сокетами на C/C++ с libevent

Помните, как когда-то мы писали простой TCP-сервер на C, а потом разбирали типичные ошибки? Описанный в этих статьях подход прекрасно работает, но только до тех пор, пока количество одновременно обслуживаемых соединений невелико — условно, пара сотен. Если же вам нужно обслуживать 10 или 50 тысяч соединений (так называемая проблема C10K), программу нужно писать совершенно иначе. Давайте разберемся, почему так, и как же нужно писать.

Суть проблемы

Почему же не работает просто держать 10 000 сокетов и обслуживать каждый из них в отдельном треде? Дело в том, что треды не бесплатны — операционная система выделяет под треды память, а также распределяет между ними время CPU. Переключение контекста конкретного ядра CPU с одного треда на другой — операция тоже не бесплатная. Это не проблема, когда тредов всего лишь несколько сотен. Но когда счет идет на тысячи тредов, больше времени начинает тратиться на переключение контекстов, чем на выполнение полезной работы.

Можно ли уменьшить число тредов и при этом продолжить обслуживать 10 000 соединений? Оказывается, что можно. Простейшее решение заключается в использовании системного вызова select(), который позволяет по списку файловых дескрипторов определить, какие из них стали готовы для заданного класса операций ввода-вывода. Возможности select(), правда, не безграничны — хорошо работает он только в случае, если файловых дескрипторов не больше сотни. Поэтому для решения той же задачи в современных операционных системах есть лишенные этого недостатка системные вызовы: в Linux это epoll, во FreeBSD — kqueue, в Windows — IOCP.

Поскольку на разных платформах системные вызовы разные, было разработано несколько библиотек, предоставляющих соответствующий слой абстракции. В качестве примеров можно привести библиотеки libevent, libev и libuv. Эти библиотеки имеют немного разные компромиссы. К примеру, libev не поддерживает Windows, потому что, действительно, кто же использует Windows на серверах, да еще и обслуживающих столько соединений?

В рамках этой заметки мы сосредоточим свое внимание на библиотеке libevent. Помимо прочего, libevent интересен тем, что имеет отличную документацию на Doxygen, кучу примеров, и даже небольшую бесплатную книжку. Библиотека используется в множестве известных проектов, таких, как Chromium, Memcached, Tor, Transmission и PgBouncer. Кроме того, у libevent есть ряд интересных фичей, например, встроенных асинхронный HTTP-сервер.

Использование libevent

Для примера напишем на базе libevent простенький чат. Клиенты цепляются к нему по TCP обычным telnet’ом и отправляют строчки. Строчку, отправленную одним клиентом, получают все остальные клиенты.

У меня код получился сравнительно большим, поэтому рассмотрим его по частям:

#define READ_BUFF_SIZE 128
#define WRITE_BUFF_SIZE ((READ_BUFF_SIZE)*8)

typedef struct connection_ctx_t <
struct connection_ctx_t * next ;
struct connection_ctx_t * prev ;

evutil_socket_t fd ;
struct event_base * base ;
struct event * read_event ;
struct event * write_event ;

uint8_t read_buff [ READ_BUFF_SIZE ] ;
uint8_t write_buff [ WRITE_BUFF_SIZE ] ;

ssize_t read_buff_used ;
ssize_t write_buff_used ;
> connection_ctx_t ;

Эта структура представляет собой соединение пользователя. Все соединения объединяются в двусвязный список (поля next, prev). Каждое соединение хранит соответствующий ему сокет (fd), буфер принятых байт (read_buff, read_buff_used), а также буфер с байтами, готовыми к отправке (write_buff, write_buff_used). Есть также поля base, read_event и write_event, смысл которых станет понятен чуть ниже.

void run ( char * host , int port ) <
// allocate memory for a connection ctx (used as linked list head)
connection_ctx_t * head_ctx = ( connection_ctx_t * ) malloc (
sizeof ( connection_ctx_t ) ) ;
if ( ! head_ctx )
error ( «malloc() failed» ) ;

head_ctx -> next = head_ctx ;
head_ctx -> prev = head_ctx ;
head_ctx -> write_event = NULL ;
head_ctx -> read_buff_used = 0 ;
head_ctx -> write_buff_used = 0 ;

// create a socket
head_ctx -> fd = socket ( AF_INET , SOCK_STREAM , 0 ) ;
if ( head_ctx -> fd 0 )
error ( «socket() failed» ) ;

// make it nonblocking
if ( evutil_make_socket_nonblocking ( head_ctx -> fd ) 0 )
error ( «evutil_make_socket_nonblocking() failed» ) ;

// bind and listen
struct sockaddr_in sin ;
sin . sin_family = AF_INET ;
sin . sin_port = htons ( port ) ;
sin . sin_addr . s_addr = inet_addr ( host ) ;
if ( bind ( head_ctx -> fd , ( struct sockaddr * ) & sin , sizeof ( sin ) ) 0 )
error ( «bind() failed» ) ;

if ( listen ( head_ctx -> fd , 1000 ) 0 )
error ( «listen() failed» ) ;

// create an event base
struct event_base * base = event_base_new ( ) ;
if ( ! base )
error ( «event_base_new() failed» ) ;

// create a new event
struct event * accept_event = event_new ( base , head_ctx -> fd ,
EV_READ | EV_PERSIST , on_accept , ( void * ) head_ctx ) ;
if ( ! accept_event )
error ( «event_new() failed» ) ;

head_ctx -> base = base ;
head_ctx -> read_event = accept_event ;

// schedule the execution of accept_event
if ( event_add ( accept_event , NULL ) 0 )
error ( «event_add() failed» ) ;

// run the event dispatching loop
if ( event_base_dispatch ( base ) 0 )
error ( «event_base_dispatch() failed» ) ;

// free allocated resources
on_close ( head_ctx ) ;
event_base_free ( base ) ;
>

Помимо обычной последовательности вызовов socket, bind, listen, созданный сокет здесь делается неблокирующим при помощи соответствующей процедуры из библиотеки libevent. Создается event_base, главный «класс» в libevent, благодаря которому осуществляется работа всего остального. Также создается событие с именем accept_event, выстреливающее, когда сокет становится доступен на чтение, то есть, в данном случае — готов принять новое соединение. Событие «включается» с помощью процедуры event_add, после чего выполнение программы уходит в недры процедуры event_base_dispatch, откуда при нормальном выполнении программы управление уже не вернется. Другими словами, этот код говорит «забиндить порт, и когда на него кто-то постучится, вызвать процедуру on_accept».

void on_accept ( evutil_socket_t listen_sock , short flags , void * arg ) <
connection_ctx_t * head_ctx = ( connection_ctx_t * ) arg ;
evutil_socket_t fd = accept ( listen_sock , 0 , 0 ) ;

if ( fd 0 )
error ( «accept() failed» ) ;

// make in nonblocking
if ( evutil_make_socket_nonblocking ( fd ) 0 )
error ( «evutil_make_socket_nonblocking() failed» ) ;

connection_ctx_t * ctx = ( connection_ctx_t * ) malloc (
sizeof ( connection_ctx_t ) ) ;
if ( ! ctx )
error ( «malloc() failed» ) ;

// add ctx to the linked list
ctx -> prev = head_ctx ;
ctx -> next = head_ctx -> next ;
head_ctx -> next -> prev = ctx ;
head_ctx -> next = ctx ;

ctx -> base = head_ctx -> base ;

ctx -> read_buff_used = 0 ;
ctx -> write_buff_used = 0 ;

printf ( «[%p] New connection! fd = %d \n » , ctx , fd ) ;

ctx -> fd = fd ;
ctx -> read_event = event_new ( ctx -> base , fd , EV_READ | EV_PERSIST ,
on_read , ( void * ) ctx ) ;
if ( ! ctx -> read_event )
error ( «event_new(. EV_READ . ) failed» ) ;

ctx -> write_event = event_new ( ctx -> base , fd , EV_WRITE | EV_PERSIST ,
on_write , ( void * ) ctx ) ;
if ( ! ctx -> write_event )
error ( «event_new(. EV_WRITE . ) failed» ) ;

if ( event_add ( ctx -> read_event , NULL ) 0 )
error ( «event_add(read_event, . ) failed» ) ;
>

Эта процедура принимает входящее соединение с помощью системного вызова accept. Также она выделяет память для структуры connection_ctx_t, полностью заполняет ее и добавляет в двусвязных список всех открытых соединений. В процессе заполнения структуры с помощью процедуры event_new создаются события read_event и write_event по аналогии с тем, как это делалось в рассмотренной выше процедуре run. Из этих двух событий с помощью event_add «включается» только read_event. Другими словами, код говорит «принять соединение, добавить его в список, и когда что-то придет, дернуть процедуру on_read».

void on_read ( evutil_socket_t fd , short flags , void * arg ) <
connection_ctx_t * ctx = arg ;

printf ( «[%p] on_read called, fd = %d \n » , ctx , fd ) ;

ssize_t bytes ;
for ( ;; ) <
bytes = read ( fd , ctx -> read_buff + ctx -> read_buff_used ,
READ_BUFF_SIZE — ctx -> read_buff_used ) ;
if ( bytes == 0 ) <
printf ( «[%p] client disconnected! \n » , ctx ) ;
on_close ( ctx ) ;
return ;
>

if ( bytes 0 ) <
if ( errno == EINTR )
continue ;

printf ( «[%p] read() failed, errno = %d, »
«closing connection. \n » , ctx , errno ) ;
on_close ( ctx ) ;
return ;
>

break ; // read() succeeded
>

ssize_t check = ctx -> read_buff_used ;
ssize_t check_end = ctx -> read_buff_used + bytes ;
ctx -> read_buff_used = check_end ;

while ( check check_end ) <
if ( ctx -> read_buff [ check ] != ‘ \n ‘ ) <
check ++;
continue ;
>

on_string_received ( ( const char * ) ctx -> read_buff , length , ctx ) ;

// shift read_buff (optimize!)
memmove ( ctx -> read_buff , ctx -> read_buff + check ,
check_end — check — 1 ) ;
ctx -> read_buff_used -= check + 1 ;
check_end -= check ;
check = 0 ;
>

if ( ctx -> read_buff_used == READ_BUFF_SIZE ) <
printf ( «[%p] client sent a very long string, »
«closing connection. \n » , ctx ) ;
on_close ( ctx ) ;
>
>

Процедура on_read вызывается, когда в одном из соединений есть данные, которые можно прочитать. Процедура считывает эти данные и помещает в read_buff. Когда в буфере оказывается строка, заканчивающаяся символом \n , он заменяется на \0 , после чего вызывается on_string_received с полученной строкой, ее длиной и информацией о соединении в качестве аргументов. После обработки полученной строки содержимое буфера сдвигается. В случае возникновения ошибок (read_buff полностью заполнился, не удалось считать данные, …) клиентское соединение закрывается.

// called manually
void on_string_received ( const char * str , int len ,
connection_ctx_t * ctx ) <
printf ( «[%p] a complete string received: ‘%s’, length = %d \n » ,
ctx , str , len ) ;

connection_ctx_t * peer = ctx -> next ;
while ( peer != ctx ) <
if ( peer -> write_event == NULL ) < // list head, skipping
peer = peer -> next ;
continue ;
>

printf ( «[%p] sending a message to %p. \n » , ctx , peer ) ;

// check that there is enough space in the write buffer
if ( WRITE_BUFF_SIZE — peer -> write_buff_used len + 1 ) <
// if it’s not, call on_close being careful with
// the links in the linked list
printf ( «[%p] unable to send a message to %p — »
«not enough space in the buffer; »
«closing %p’s connection \n » ,
ctx ,
peer ,
peer ) ;
connection_ctx_t * next = peer -> next ;
on_close ( peer ) ;
peer = next ;
continue ;
>

// append data to the buffer
memcpy ( peer -> write_buff + peer -> write_buff_used , str , len ) ;
peer -> write_buff [ peer -> write_buff_used + len ] = ‘ \n ‘ ;
peer -> write_buff_used += len + 1 ;

// add writing event (it’s not a problem to call it
// multiple times)
if ( event_add ( peer -> write_event , NULL ) 0 )
error ( «event_add(peer->write_event, . ) failed» ) ;

Процедура on_string_received берет принятую от одного из клиентов строку и записывает ее в конец write_buff остальных клиентов. Также для этих клиентов происходит «включение» событий write_event с помощью процедуры event_add. Таким образом мы говорим, что готовы что-то послать клиентам, если, конечно, они готовы что-то принять. В случае возникновения ошибки, например, нехватки места в write_buff, соответствующее соединение закрывается.

void on_write ( evutil_socket_t fd , short flags , void * arg ) <
connection_ctx_t * ctx = arg ;
printf ( «[%p] on_write called, fd = %d \n » , ctx , fd ) ;

ssize_t bytes ;
for ( ;; ) <
bytes = write ( fd , ctx -> write_buff , ctx -> write_buff_used ) ;
if ( bytes 0 ) <
if ( errno == EINTR )
continue ;

Илон Маск рекомендует:  Что такое код memccpy

printf ( «[%p] write() failed, errno = %d, »
«closing connection. \n » , ctx , errno ) ;
on_close ( ctx ) ;
return ;
>

break ; // write() succeeded
>

// shift the write_buffer (optimize!)
memmove ( ctx -> write_buff , ctx -> write_buff + bytes ,
ctx -> write_buff_used — bytes ) ;
ctx -> write_buff_used -= bytes ;

// if there is nothing to send call event_del
if ( ctx -> write_buff_used == 0 ) <
printf ( «[%p] write_buff is empty, »
«calling event_del(write_event) \n » , ctx ) ;
if ( event_del ( ctx -> write_event ) 0 )
error ( «event_del() failed» ) ;
>
>

Процедура on_write вызывается при включенном событии write_event (то есть, когда write_buff не пуст), когда сокет готов к передаче данных. Процедура просто записывает в сокет столько, сколько получится записать, и сдвигает write_buff на соответствующее количество байт. Если буфер при этом становится пустым, событие write_event выключается с помощью процедуры event_del. Если этого не сделать, on_write будет постоянно вызываться с пустым буфером на отправку, лишь зря грея CPU.

// called manually
void on_close ( connection_ctx_t * ctx ) <
printf ( «[%p] on_close called, fd = %d \n » , ctx , ctx -> fd ) ;

// remove ctx from the lined list
ctx -> prev -> next = ctx -> next ;
ctx -> next -> prev = ctx -> prev ;

event_del ( ctx -> read_event ) ;
event_free ( ctx -> read_event ) ;

if ( ctx -> write_event ) <
event_del ( ctx -> write_event ) ;
event_free ( ctx -> write_event ) ;
>

close ( ctx -> fd ) ;
free ( ctx ) ;
>

Наконец, on_close вызывается при закрытии соединения — штатном или в случае той или иной ошибки. Информация о соединении удаляется из двусвязного списка, все выделенные ресурсы освобождаются, сокет закрывается.

Как видите, приведенный код довольно прост, во всяком случае, концептуально. Я протестировал его на Linux и FreeBSD как локально, так и по сети, и не смог выявить каких-либо дефектов. Желающие могут проверить его самостоятельно, в том числе и на Windows, и в случае необходимости прислать мне pull request. Репозиторий, как обычно, вы найдете на GitHub. Помимо приведенного выше кода в репозитории вы также найдете и небольшую тестовую утилиту, написанную на Go.

Заключение

Приведенный материал не претендует на рассмотрение абсолютно всех возможностей libevent. Помимо прочего, за кадром остались такие возможности, как I/O Buffers, фреймворк для построения RPC, функции для асинхронной работы с DNS, а также уже упомянутый асинхронный HTTP-сервер. В качестве домашнего задания вы можете изучить какую-нибудь из этих возможностей. Документацию и примеры вы найдете на официальном сайте libevent’а.

Также вы можете доработать мой пример, сделав из него, к примеру, очередь сообщений типа RabbitMQ — с топиками, подписками на них, и вот этим вот всем. При желании можно даже реализовать протокол AMQP. Вообще, если вы ищите идею для проекта на базе libevent или одной из его альтернатив, можно взять произвольный протокол (HTTP/2, WebSockets, Socks5, SMTP, FTP, IRC, …) и реализовать соответствующий сервер, клиент, или и то, и другое.

А как вы пишиште кроссплатформенные асинхронные сетевые приложения на C и C++?

petukhovsky.com

Или как работать с WebSocket на простом PHP хостинге

Или getting started with WebSocket PHP без phpDaemon

Здравствуйте! Простите за столь длинный заголовок, но, надеюсь, что новичкам вроде меня будет легче найти эту статью, ведь мне ничего подобного найти не удалось. Несколько недель назад я принял решение переработать игровой клиент и сервер своей игры Growing Crystals с AJAX, на WebSocket, но всё оказалось не просто непросто, а очень сложно. Поэтому я и решил написать статью, которая бы помогла самым что ни на есть начинающим разработчикам на WebSocket + PHP сэкономить несколько дней времени, максимально подробно объясняя каждый свой шаг по настройке и запуску первого WebSocket скрипта на PHP.

Что у меня есть: Денвер на локальной машине, на нём я веду разработку проекта и дешевый PHP хостинг, на котором я публикую свой проект для того, чтобы получить обратную связь от Интернет-пользователей.

Что я хочу: Без установки phpDaemon (phpd), NodeJS и прочих вещей на локальную машину и хостинг, продолжить разработку своего проекта, но теперь с WebSocket, в этой статье разберем простой WebSocket эхо сервер.

Чего я не хочу: Говоря о NodeJS, не хочется переписывать серерную логику с PHP на другой язык, тем более устанавливать NodeJS, хотя и люблю JavaScript больше чем PHP.

Важно: не путайте демона написанного на php, с фреймворком асинхронных приложений phpDaemon. Который, конечно же, обязательно потребуется в случае развития проекта и многократного роста нагрузки на хостинг. Но для начала работы с WebSocket на дешевом хостинге можно обойтись и без него.

Проверка поддержки сокетов на хостинге и в Денвере

— Что нужно сделать в первую очередь для начала работы с WebSocket?
— Проверить поддержку сокетов на хостинге и в Денвере.

Для этого создаём простенький файлик sockettest.php

Загружаем на хостинг и обращаемся к нему через браузер, и, здесь может быть самое неприятное, хостинг выдаст WebSockets UNAVAILABLE. В таком случае нужно искать другой хостинг, либо, если есть возможность поменять в настройках хостинга версию и комплектацию PHP на PHP с sockets, добиваться того, чтобы хотсер волшебным образом это сделал. Если хостер такой возможности не предоставляет, то тогда при выборе нового хостинга, нужно смотреть на php_info(); и по нему определить поддержку сокетов по наличию следующей строки. А для того, чтобы сокеты заработали в Денвере, пришлось немного покопаться.

В моём случае хостинг заработал с первой попытки, что в 2014 году вероятно на 80%.

Добавление веб-сокетов в Денвер

Теперь задача настроить Денвер. Мне довелось работать с разными сборками Денвера. Последняя из них Denwer3_Base_2013-06-02_a2.2.22_p5.3.13_m5.5.25_pma3.5.1_xdebug.exe, к сожалению, она и другие имеющиеся в настоящий момент сборки Денвера(дело в PHP) не поддерживают сокеты по умолчанию.

Но эта проблема решается путём поиска и установки подходящей php_sockets.dll. Для того, чтобы всё заработало, достаточно разместить dll файл в каталоге Денвера \usr\local\php5\ext\php_sockets.dll и отредактировать файл \usr\local\php5\php.ini убрав точку с запятой перед строкой

перезагрузить Денвер и всё, или почти всё: в некоторых случаях при перезагрузке Денвера может возникнуть ошибка

это значит что вы используете не подходящую версию файла php_sockets.dll. Чтобы облегчить поиски предлагаю две версии — одна гарантированно подходит для PHP 5.2.12 (скачать), а вторая для гарантированно подходит для PHP 5.3.13 (скачать). Но если вам не подошел ни один из этих файлов, предлагаю скачать полный архив соответствующей версии php для windows с веб-сайта php.net и найти в архиве файл php_sockets.dll, который точно подойдёт.

Теперь можете запустить файлик sockettest.php через браузер и увидеть заветную строку «WebSockets OK», означающую что всё необходимое для работы с Web-Socket установлено и работает хорошо.

От сокета к веб-сокету

Теперь, когда модуль сокетов активирован в PHP, нам предстоит путь от написания простого сокет скрипта, до скрипта способного корректно общаться по протоколу веб-сокет. Дело в том, что PHP умеет только технически работать с сокетами: принимать соедние, заводить очередь, отправлять/принимать данные, находиться в режиме ожидания и другие технические вещи. Но в нём совершенно отсутствуют готовые заголовки и другие метаданные необходимые для веб-сокетов. Обычное сокет-соединение отличается от веб-сокетов тем, что мы можем отправлять любые сообщения в произвольном формате, в то время, как веб-сокет требует обязательной отправки заголовков вида «GET / HTTP/1.1\r\nUpgrade: websocket\r\n» и соблюдение достаточного количества других правил.

Есть еще один важный нюанс, теперь касающийся PHP — скрипт обрабатывающий сокет-соединения отличается от обычного скрипта PHP, который многократно выполняется от начала до конца при разгрузке страницы обычно менее чем за 0,1 секунды. Отличаются скрипты работы с WebSocket-ами тем, что длительность их выполнения должна быть бесконечной. Т.е. мы должны инициировать выполнение PHP-скрипта содержащего бесконечный цикл, в котором происходит получение/отправка сообщений по протоколу веб-сокет. На самом деле с этим не должно быть никаких проблем, т.к. существует большое количество способов снять ограничение по времени выполнения PHP скрипта, помимо этого можно запустить скрипт в бэкграунде пользуясь консолью сервера. На PHP вполне возможно даже создание полноценного демона. Существуют готовые решения, о которых я упоминал выше, например PhpDaemon, но сегодня речь не о них.

Для начала, чтобы убедиться что мне не мешают файрволлы, всё настроено правильно и связь между клиентом и сервером может быть установлена, я решил написать и протестировать небольшой PHP скрипт выполняющий роль сокет-сервера (именно сокет, а не веб-сокет!), устанавливающий соединение и отвечающий всем фразой «Hello, Client!». Но для его тестирования нужно несколько клиентов (веб-сокет клиент, чтобы понять базовое отличие от простого сокета и обычный telnet).

Клиент на html+JavaScript для веб-сокет соединения

Для тестирования скрипта сокет-сервера нам понадобится несколько клиентов, один из них telnet (я использовал Putty), второй, веб-сокет клиент, написанный на html+JavaScript. Вы можете не устанавливать себе Telnet, его я использовал исключительно, чтобы разобраться в том, что отправляет сервер и почему я не могу это увидеть в браузере. Приведу код веб-сокет клиента, написанного на html+JavaScript.

Веб-сокет клиент должен иметь возможность подключаться/отключаться к веб-сокетам, отправлять сообщения, выводить полученные ответы. По умолчанию, в качестве веб-сокет сервера выступает ws://echo.websocket.org, т.к. это гарантированно работающий ws(веб-сокет, далее везде ws) echo сервер, на котором можно убедиться в работоспособности нашего веб-сокет клиента.

Логика скрипта JavaScript также максимально проста. При загрузке пытаемся подключиться по адресу ws сервера по умолчанию. Выполняем функции приёма отправки сообщений. Я специально для простоты понимания кода не использую jQuery и другие библиотеки. В скрипте используются команды создания веб-сокета (что означает автоматическое подключение), отправки сообщения и закрытия.

и обработчики событий onopen, onmessage. Которые вызываются при открытии соединения и при получении сообщения соответственно. К сожалению, если вы создадите эти два файла и запустите клиента через Денвер с localhost, то у вас возникнут проблемы с кодировкой т.к. Денвер по умолчанию использует CP1251, не забывайте явно прописать UTF-8 в .htaccess. Или скачайте архив данного ws клиента, содержащий также и сокет-сервер.

Код сокет-сервера на PHP я приводить не буду, т.к. он есть в архиве и снабжен комментариями. Я искусственно ограничил время работы приёмки соединений 100 секундами, чтобы скрипт он не оставался в памяти, закрывал все соединения и не приходилось при внесении в него изменений постоянно перезагружать Денвер. При чем, по прошествии 100 секунд, скрипт не прекратит свою работу, а будет ждать подключения, после которого его работа будет завершена, а все сокеты благополучно закрыты. Также, для тестирования, я использую localhost и порт 889.

Файлы из архива можно поместить в корневую папку localhost Денвера. Алгоритм тестирования:

  1. Запускаем http://localhost/socket.html, он автоматически подключится к ws echo серверу ws://echo.websocket.org, можно отправить несколько сообщений, которые сервер должен отправить обратно, ведь это же эхо-сервер. Если это работает — отлично, с клиентом всё в порядке, если нет, проверьте все ли вы файлы разместили в одном каталоге, запущен ли у вас Денвер, и есть ли соединение с сетью Интернет.
  2. Откройте в этой же вкладке где у вас открыт ws-клиент JavaScript консоль (В GoogleChrome 34.8.1847.137 m и в FireFox это делается почти одинаково меню->инструменты->консоль Java Script или Ctrl+Shift+J, но лично я использую для этого консоль FireBug). Отправьте еще несколько сообщений. Вы увидите какие сообщения приходят от сервера. Вы можете нажать disconnect и после этого отправить несколько сообщений, вы убедитесь что сообщения не уходят, т.к. связь с сервером ws://echo.websocket.org разорвана.
  3. Запускаем в новой вкладке браузера наш сокет-сервер http://localhost/simpleworking.php. Желательно чтобы вы сразу могли видеть и вкладку клиента с консолью и вкладку сервера. Должно появиться

Что означает, что сервер готов к входящим соединениям.
Во вкладке клиента в поле Server address вводим ws://127.0.0.1:889 и нажимаем reconnect, мы видим что на клиенте ничего не происходит, а на сервере появляются сообщения вида

Что говорит нам о том, что технически соединение с сокетом на сервере установлено, а на клиенте ожидается соединение по протоколу веб-сокет, и оно не установлено, т.к. браузером не получены соответствующие заголовки протокола ws. При попытке отправить сообщение с клиента, в консоли должна появиться ошибка о том, что соедние с ws не установлено. К сожалению, скрипт в GoolgeChrome остановится и для новых попыток подключения придётся перезагружать страницу с веб-клиентом. В FireFox скрипт продолжает выполняться. Обязательно сделайте reconnect спустя 100 секунд, после запуска скрипта сервера, чтобы дать ему благополучно завершиться.

  • Чтобы окончательно убедиться в том, что сервер отвечает, что его сообщения не блокируются файрволом, запустите скрипт сервера http://localhost/simpleworking.php запустите telnet и попытайтесь подключиться к 127.0.0.1:889, только это нужно сделать не позднее 100 секунд, с момента запуска сервера, пока он не закрыл соединения и не завершил скрипт.
  • По telnet должен придти ответ «Hello, Client!», что свидетельствует о том, что всё работает в штатном режиме и связь с сервером двухсторонняя.

    Ответ сокет сервера при попытке подключения по Telnet

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

    Протокол веб-сокет

    Теперь остаётся научить наш сокет-сервер общаться как веб-сокет сервер, корректно работать в фоне (демон), и, самое главное, на дешевом хостинге. Немного теории: разрешив серверу выводить на экран сообщение, которое получаем от клиента при попытке подключения, добавив перед строкой $msg = «Hello, Client!»; код

    можно увидеть, что попытка установить соединение по протоколу веб-сокет всегда сопровождается отправлением клиентом заголовка с обязательным соблюдением формата протокола WebScoket. Тег pre для вывода заголовков выбран не случайно, т.к. в заголовках большое значение играют переносы строк. Такие заголовки я получаю от браузеров, когда пытаюсь подключиться к нашему ws://127.0.0.1:889.

    Еще до того, как я начал изучать веб-сокеты, мне казалось, что работа с веб-сокетами будет напоминать работу с AJAX, когда мы отправляли запросы серверу используя JavaScript XMLHttpRequest(), но в реальности задача оказалась намного сложнее, и в первую очередь потому, что WebSocket это протокол находящийся на одном уровне с протоколом HTTP (но с некоторыми нюансами) в сетевой модели OSI, что означает, нам на стороне сервера, нужно реализовывать функционал похожий на обработку HTTP (который всегда выполнял Apache). Продолжая сравнения с AJAX, в AJAX нам не нужно обеспечивать работу протокола HTTP, т.к. это всё делает браузер и веб-сервер Apache. Мы просто отправляем и получаем сообщения. Но, использование ws хоть и накладывает на нас достаточно большой объём работы связанный с реализацией сервера на PHP, оно также даёт и преимущества: сокращение объёма передаваемых данных и рост производительности серверного ПО. Для успешного общения с клиентами по протоколу WebSocket сервер должен строго выполнять требования стандарта RFC6455, подробно описывающего форматы заголовков ответов. Все сообщения отправляемые по протоколу WebSocket можно разделить на несколько видов: handsnake (рукопожатие при установлении связи), ping-pong(проверка связи) и data-transfer(передача данных). Также есть более краткое описание протокола в общих чертах на русском. Для того, чтобы обеспечить полноценное корректное общение сервера и клиента по протоколу веб-сокет, необходима реализация функций на PHP получения и разбора заголовков от клиента, составление заголовков сервером, составление ключа и ряд других. Изучив материалы представленные на хабре и других ресурсах, удалось найти реализованные годные функции, единственным смущающим различием явлилось то, что большинство авторов используют потоковые функции работы с сокетами, они считаются более компактными, но, при этом, более ресурсоёмкими в связи с использованием буфера. Для перехода на потоковые функции работы с сокетами, не требуется установка никаких дополнительных модулей кроме уже установленных, т.к. потоки встроены в PHP 5 по умолчанию. Потоковые функции работы с сокетами всегда начинаются с stream_socket_*. При использовании потоковых функций, желательно убедиться в phpinfo() в поддержке необходимого транспорта для потоков.

    В случае если такой информации в phpinfo() нет или в случае возникновения других проблем, обратитесь к документации, но проблема решается банальным обновлением Денвера на актуальную версию.
    Еще важным является тот факт, что большинство систем ограничивает возможность создания сокета на порту ниже чем 1024, поэтому во всех дальнейших программах для сокета будет использоваться порт 8889.
    Взяв за основу исходные коды функций кодирования и декодирования заголовков протокола WebSocket, удалось реализовать полноценный ws echo сервер. Алгоритм его работы также прост как и в предыдущем случае: создаём ws сервер используя функцию stream_socket_server, запускаем бесконечный цикл в котором проверяем наличие новых соединений и при получении нового соединения размещаем его в массиве $connects, также запускаем второй цикл, который пробегает по всем соединениям и закрывает отвалившиеся и получает сообщения от открытых соединений. Также в скрипт PHP мною добавлено три функции, которые я оформил как обработчиков событий.

    Которые ничего не делают, за исключением вывода сообщений о событиях на экран. И при получении сообщения от клиента, раскодируем его и отправляем его текст, предварительно закодировав, обратно. Архив ws клиента и ws echo сервера, ws клиент не изменился. Можно протестировать ws echo server скачав архив, и разместив его в корневой папке localhost-а Денвера. Подключиться клиентом к адресу ws://127.0.0.1:8889.
    Тонкости связанные с запуском ws сервера в фоне (демон), и запуск на хостинге мы разберем в следующей статье. Буду рад комментариям и отзывам.

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

    Пример работы с сокетами для window

    Socket (гнездо, разъем) — абстрактное программное понятие, используемое для обозначения в прикладной программе конечной точки канала связи с коммуникационной средой, образованной вычислительной сетью. При использовании протоколов TCP/IP можно говорить, что socket является средством подключения прикладной программы к порту локального узла сети.

    Socket-интерфейс представляет собой просто набор системных вызовов и/или библиотечных функций языка программирования СИ, разделенных на четыре группы:

    Ниже рассматривается подмножество функций socket-интерфейса, достаточное для написания сетевых приложений, реализующих модель «клиент-сервер» в режиме с установлением соединения. Все функции сокетов содержаться в wsock32.dll перед тем как написать программу с использование функций сокетов необходимо задать компилятору чтобы он включил в программу файл wsock32.lib. В меню Рroject выберите пункт settings а там укажите раздел Link, wsock32.lib можно ввести в поле Library modules или project options. Не забудьте поставить #include «Winsock2.h».

    1. Создание сервера

    Прежде чем воспользоваться функцией socket необходимо проинициализировать процесс библиотеки wsock32.dll вызвав функцию WSAStartup например: Здесь 0х0101 версия библиотеки которую следует использовать.

    Теперь объявление переменную типа SOCKET например s :

      s = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

    Создание socket’а осуществляется следующим системным вызовом

    Аргумент domain задает используемый для взаимодействия набор протоколов (вид коммуникационной области), для стека протоколов TCP/IP он должен иметь символьное значение AF_INET.

    Аргумент type задает режим взаимодействия:

    • SOCK_STREAM — с установлением соединения;
    • SOCK_DGRAM — без установления соединения.

    Аргумент protocol задает конкретный протокол транспортного уровня (из нескольких возможных в стеке протоколов). Если этот аргумент задан равным 0, то будет использован протокол «по умолчанию» (TCP для SOCK_STREAM и UDP для SOCK_DGRAM при использовании комплекта протоколов TCP/IP).

    При удачном завершении своей работы данная функция возвращает дескриптор socket’а — целое неотрицательное число, однозначно его идентифицирующее. Дескриптор socket’а аналогичен дескриптору файла ОС UNIX.

    При обнаружении ошибки в ходе своей работы функция возвращает число «-1».

    Далее мы задаем параметры для сокета (сервера) для этого нам необходимо объявить структуру SOCKADDR_IN sin далее заполняем параметры для сервера:

    Структура SOCKADDR_IN используется несколькими системными вызовами и функциями socket-интерфейса и определена в include-файле in.h следующим образом:

    Поле sin_family определяет используемый формат адреса (набор протоколов), в нашем случае (для TCP/IP) оно должно иметь значение AF_INET.

    Поле sin_addr содержит адрес (номер) узла сети.

    Поле sin_port содержит номер порта на узле сети.

    Поле sin_zero не используется.

    Определение структуры in_addr (из того же include-файла) таково:

    Структура SOCKADDR_IN должна быть полностью заполнена перед выдачей системного вызова bind . При этом, если поле sin_addr.s_addr имеет значение INADDR_ANY, то системный вызов будет привязывать к socket’у номер (адрес) локального узла сети.

    Для подключения socket’а к коммуникационной среде, образованной вычислительной сетью, необходимо выполнить системный вызов bind , определяющий в принятом для сети формате локальный адрес канала связи со средой. В сетях TCP/IP socket связывается с локальным портом. Системный вызов bind имеет следующий синтаксис:

    Аргумент s задает дескриптор связываемого socket’а.

    Аргумент addr в общем случае должен указывать на структуру данных, содержащую локальный адрес, приписываемый socket’у. Для сетей TCP/IP такой структурой является SOCKADDR_IN.

    Аргумент addrlen задает размер (в байтах) структуры данных, указываемой аргументом addr.

    В случае успеха bind возвращает 0, в противном случае — «-1».

    Для установления связи «клиент-сервер» используются системные вызовы listen и accept (на стороне сервера), а также connect (на стороне клиента). Для заполнения полей структуры socaddr_in, используемой в вызове connect , обычно используется библиотечная функция gethostbyname, транслирующая символическое имя узла сети в его номер (адрес).

    Системный вызов listen выражает желание выдавшей его программы-сервера ожидать запросы к ней от программ-клиентов и имеет следующий вид:

    Пример:

      err = listen( s, SOMAXCONN);

    Аргумент s задает дескриптор socket’а, через который программа будет ожидать запросы к ней от клиентов. Socket должен быть предварительно создан системным вызовом socket и обеспечен адресом с помощью системного вызова bind .

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

    Признаком удачного завершения системного вызова listen служит нулевой код возврата.

    Перед тем как воспользоваться функцией accept сначала объявите ещё одну переменную типа SOCKET, например s1 . Это зделано для того что бы узнать IP адрес и порт удаленного компьютера. Что бы вывести на экран IP адрес и порт удаленного компа просто вставить в программу такие строки:

      printf(«accepted connection from %s, port %d\n», inet_ntoa(from.sin_addr), htons(from.sin_port)) ;

    Для приема запросов от программ-клиентов на установление связи в программах-серверах используется системный вызов accept , имеющий следующий вид:

    Аргумент s задает дескриптор socket’а, через который программа-сервер получила запрос на соединение (посредством системного запроса listen ).

    Аргумент addr должен указывать на область памяти, размер которой позволял бы разместить в ней структуру данных, содержащую адрес socket’а программы-клиента, сделавшей запрос на соединение. Никакой инициализации этой области не требуется.

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

    Системный вызов accept извлекает из очереди, организованной системным вызовом listen , первый запрос на соединение и возвращает дескриптор нового (автоматически созданного) socket’а с теми же свойствами, что и socket, задаваемый аргументом s. Этот новый дескриптор необходимо использовать во всех последующих операциях обмена данными.

    Кроме того после удачного завершения accept :

    1. область памяти, указываемая аргументом addr, будет содержать структуру данных (для сетей TCP/IP это sockaddr_in), описывающую адрес socket’а программы-клиента, через который она сделала свой запрос на соединение;
    2. целое число, на которое указывает аргумент p_addrlen, будет равно размеру этой структуры данных.

    Если очередь запросов на момент выполнения accept пуста, то программа переходит в состояние ожидания поступления запросов от клиентов на неопределенное время (хотя такое поведение accept можно и изменить).

    Признаком неудачного завершения accept служит отрицательное возвращенное значение (дескриптор socket’а отрицательным быть не может).

    Примечание. Системный вызов accept используется в программах-серверах, функционирующих только в режиме с установлением соединения.

    Вот мы соединились с клиентом, но как получить и передать информацию? Это делается с помощью команд send и recv ко не забывайте что вы теперь работаете с переменной s1: Мы поставили цикл while, потому что он будет выполнятся пока клиент не отключится

    Для получения данных от партнера по сетевому взаимодействию используется системный вызов recv , имеющий следующий вид

    Аргумент s задает дескриптор socket’а, через который принимаются данные.

    Аргумент buf указывает на область памяти, предназначенную для размещения принимаемых данных.

    Аргумент len задает длину (в байтах) этой области.

    Аргумент flags модифицирует исполнение системного вызова recv . При нулевом значении этого аргумента вызов recv полностью аналогичен системному вызову read .

    При успешном завершении recv возвращает количество принятых в область, указанную аргументом buf , байт данных. Если канал данных, определяемый дескриптором s, оказывается «пустым», то recv переводит программу в состояние ожидания до момента появления в нем данных.

    Для посылки данных партнеру по сетевому взаимодействию используется системный вызов send , имеющий следующий вид


    Аргумент s задает дескриптор socket’а, через который посылаются данные.

    Аргумент buf указывает на область памяти, содержащую передаваемые данные.

    Аргумент len задает длину (в байтах) передаваемых данных.

    Аргумент flags модифицирует исполнение системного вызова send . При нулевом значении этого аргумента вызов send полностью аналогичен системному вызову write .

    При успешном завершении send возвращает количество переданных из области, указанной аргументом buf, байт данных. Если канал данных, определяемый дескриптором s, оказывается «переполненным», то send переводит программу в состояние ожидания до момента его освобождения.

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

    Аргумент s задает дескриптор ранее созданного socket’а.

    Однако в режиме с установлением логического соединения (обеспечивающем, как правило, надежную доставку данных) внутрисистемные механизмы обмена будут пытаться передать/принять данные, оставшиеся в канале передачи на момент закрытия socket’а. На это может потребоваться значительный интервал времени, неприемлемый для некоторых приложений. В такой ситуации необходимо использовать описываемый далее системный вызов shutdown .

    2. Создание клиента

    Для обращения программы-клиента к серверу с запросом на установление логической соединения используется системный вызов connect , имеющий следующий вид

    Аргумент s задает дескриптор socket’а, через который программа обращается к серверу с запросом на соединение. Socket должен быть предварительно создан системным вызовом socket и обеспечен адресом с помощью системного вызова bind .

    Аргумент addr должен указывать на структуру данных, содержащую адрес, приписанный socket’у программы-сервера, к которой делается запрос на соединение. Для сетей TCP/IP такой структурой является sockaddr_in. Для формирования значений полей структуры sockaddr_in удобно использовать функцию gethostbyname .

    Аргумент addrlen задает размер (в байтах) структуры данных, указываемой аргументом addr.

    Для того, чтобы запрос на соединение был успешным, необходимо, по крайней мере, чтобы программа-сервер выполнила к этому моменту системный вызов listen для socket’а с указанным адресом.

    При успешном выполнении запроса системный вызов connect возвращает 0, в противном случае — «-1» (устанавливая код причины неуспеха в глобальной переменной errno).

    Примечание. Если к моменту выполнения connect используемый им socket не был привязан к адресу посредством bind , то такая привязка будет выполнена автоматически.

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

    Вот наконец установлена долгожданная связь c сервером( не забывайте проверять ошибки). Дальше воспользуемся функциями send и recv по своему усмотрению.

    3. Приложение

    Для получения адреса узла сети TCP/IP по его символическому имени используется библиотечная функция

    Аргумент name задает адрес последовательности литер, образующих символическое имя узла сети.

    При успешном завершении функция возвращает указатель на структуру hostent, определенную в include-файле netdb.h и имеющую следующий вид

    Поле h_name указывает на официальное (основное) имя узла.

    Поле h_aliases указывает на список дополнительных имен узла (синонимов), если они есть.

    Поле h_addrtype содержит идентификатор используемого набора протоколов, для сетей TCP/IP это поле будет иметь значение AF_INET.

    Поле h_lenght содержит длину адреса узла.

    Поле h_addr указывает на область памяти, содержащую адрес узла в том виде, в котором его используют системные вызовы и функции socket-интерфейса.

    Для «экстренного» закрытия связи с партнером (путем «сброса» еще не переданных данных) используется системный вызов shutdown , выполняемый перед close и имеющий следующий вид

    Аргумент s задает дескриптор ранее созданного socket’а.

    Аргумент how задает действия, выполняемые при очистке системных буферов socket’а:

    Сокеты

    — это один конец двустороннего канала связи между двумя программами, работающими в сети. Соединяя вместе два сокета, можно передавать данные между разными процессами (локальными или удаленными). Реализация сокетов обеспечивает инкапсуляцию протоколов сетевого и транспортного уровней.

    Первоначально сокеты были разработаны для UNIX в Калифорнийском университете в Беркли. В UNIX обеспечивающий связь метод ввода-вывода следует алгоритму open/read/write/close. Прежде чем ресурс использовать, его нужно открыть, задав соответствующие разрешения и другие параметры. Как только ресурс открыт, из него можно считывать или в него записывать данные. После использования ресурса пользователь должен вызывать метод Close(), чтобы подать сигнал операционной системе о завершении его работы с этим ресурсом.

    Когда в операционную систему UNIX были добавлены средства межпроцессного взаимодействия (Inter-Process Communication, IPC) и сетевого обмена, был заимствован привычный шаблон ввода-вывода. Все ресурсы, открытые для связи, в UNIX и Windows идентифицируются дескрипторами. Эти дескрипторы, или описатели (handles), могут указывать на файл, память или какой-либо другой канал связи, а фактически указывают на внутреннюю структуру данных, используемую операционной системой. Сокет, будучи таким же ресурсом, тоже представляется дескриптором. Следовательно, для сокетов жизнь дескриптора можно разделить на три фазы: открыть (создать) сокет, получить из сокета или отправить сокету и в конце концов закрыть сокет.

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

    Типы сокетов

    Существуют два основных типа сокетов — потоковые сокеты и дейтаграммные.

    Потоковые сокеты (stream socket)

    Потоковый сокет — это сокет с установленным соединением, состоящий из потока байтов, который может быть двунаправленным, т, е. через эту конечную точку приложение может и передавать, и получать данные.

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

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

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

    Потоки базируются на явных соединениях: сокет А запрашивает соединение с сокетом В, а сокет В либо соглашается с запросом на установление соединения, либо отвергает его.

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

    Сервер электронной почты представляет пример приложения, которое должно доставлять содержание в правильном порядке, без дублирования и пропусков. Потоковый сокет рассчитывает, что TCP обеспечит доставку сообщений по их назначениям.

    Дейтаграммные сокеты (datagram socket)

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

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

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

    Кроме двух рассмотренных типов существует также обобщенная форма сокетов, которую называют необрабатываемыми или сырыми.

    Сырые сокеты (raw socket)

    Главная цель использования сырых сокетов состоит в обходе механизма, с помощью которого компьютер обрабатывает TCP/IP. Это достигается обеспечением специальной реализации стека TCP/IP, замещающей механизм, предоставленный стеком TCP/IP в ядре — пакет непосредственно передается приложению и, следовательно, обрабатывается гораздо эффективнее, чем при проходе через главный стек протоколов клиента.

    По определению, — это сокет, который принимает пакеты, обходит уровни TCP и UDP в стеке TCP/IP и отправляет их непосредственно приложению.

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

    Однако нечасто может потребоваться программа, работающая с сырыми сокетами. Если вы не пишете системное программное обеспечение или программу, аналогичную анализатору пакетов, вникать в такие детали не придется. Сырые сокеты главным образом используются при разработке специализированных низкоуровневых протокольных приложений. Например, такие разнообразные утилиты TCP/IP, как trace route, ping или arp, используют сырые сокеты.

    Работа с сырыми сокетами требует солидного знания базовых протоколов TCP/UDP/IP.

    Порты

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

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

    За определенными службами номера портов зарезервированы — это широко известные номера портов, например порт 21, использующийся в FTP. Ваше приложение может пользоваться любым номером порта, который не был зарезервирован и пока не занят. Агентство Internet Assigned Numbers Authority (IANA) ведет перечень широко известных номеров портов.

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

    Например, на стороне клиента, приложение должно знать адрес цели и номер порта. Отправляя запрос на соединение, клиент пытается установить соединение с сервером:

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

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

    Работа с сокетами в .NET

    Поддержку сокетов в .NET обеспечивают классы в пространстве имен System.Net.Sockets — начнем с их краткого описания.

    Классы для работы с сокетами

    Класс Описание
    MulticastOption Класс MulticastOption устанавливает значение IP-адреса для присоединения к IP-группе или для выхода из нее.
    NetworkStream Класс NetworkStream реализует базовый класс потока, из которого данные отправляются и в котором они получаются. Это абстракция высокого уровня, представляющая соединение с каналом связи TCP/IP.
    TcpClient Класс TcpClient строится на классе Socket, чтобы обеспечить TCP-обслуживание на более высоком уровне. TcpClient предоставляет несколько методов для отправки и получения данных через сеть.
    TcpListener Этот класс также построен на низкоуровневом классе Socket. Его основное назначение — серверные приложения. Он ожидает входящие запросы на соединения от клиентов и уведомляет приложение о любых соединениях.
    UdpClient UDP — это протокол, не организующий соединение, следовательно, для реализации UDP-обслуживания в .NET требуется другая функциональность.
    SocketException Это исключение порождается, когда в сокете возникает ошибка.
    Socket Последний класс в пространстве имен System.Net.Sockets — это сам класс Socket. Он обеспечивает базовую функциональность приложения сокета.

    Класс Socket

    Класс Socket играет важную роль в сетевом программировании, обеспечивая функционирование как клиента, так и сервера. Главным образом, вызовы методов этого класса выполняют необходимые проверки, связанные с безопасностью, в том числе проверяют разрешения системы безопасности, после чего они переправляются к аналогам этих методов в Windows Sockets API.

    Прежде чем обращаться к примеру использования класса Socket, рассмотрим некоторые важные свойства и методы этого класса:

    Сокеты

    Класс Socket

    В основе межсетевых взаимодействий по протоколам TCP и UDP лежат сокеты. В .NET сокеты представлены классом System.NET.Sockets.Socket , который предоставляет низкоуровневый интерфейс для приема и отправки сообщений по сети.

    Рассмотрим основные свойства данного класса:

    AddressFamily : возвращает все адреса, используемые сокетом. Данное свойство представляет одно из значений, определенных в одноименном перечислении AddressFamily . Перечисление содержит 18 различных значений, наиболее используемые:

    InterNetwork : адрес по протоколу IPv4

    InterNetworkV6 : адрес по протоколу IPv6

    Ipx : адрес IPX или SPX

    NetBios : адрес NetBios

    Available : возвращает объем данных, которые доступны для чтения

    Connected : возвращает true, если сокет подключен к удаленному хосту

    LocalEndPoint : возвращает локальную точку, по которой запущен сокет и по которой он принимает данные

    ProtocolType : возвращает одно из значений перечисления ProtocolType , представляющее используемый сокетом протокол. Есть следующие возможные значения:

    IPSecAuthenticationHeader (Заголовок IPv6 AH)

    IPSecEncapsulatingSecurityPayload (Заголовок IPv6 ESP)

    IPv6DestinationOptions (Заголовок IPv6 Destination Options)

    IPv6FragmentHeader (Заголовок IPv6 Fragment)

    IPv6HopByHopOptions (Заголовок IPv6 Hop by Hop Options)

    IPv6NoNextHeader (Заголовок IPv6 No next)

    IPv6RoutingHeader (Заголовок IPv6 Routing)

    Unknown (неизвестный протокол)

    Unspecified (неуказанный протокол)

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

    RemoteEndPoint : возвращает адрес удаленного хоста, к которому подключен сокет

    SocketType : возвращает тип сокета. Представляет одно из значений из перечисления SocketType :

    Dgram : сокет будет получать и отправлять дейтаграммы по протоколу Udp. Данный тип сокета работает в связке с типом протокола — Udp и значением AddressFamily.InterNetwork

    Raw : сокет имеет доступ к нижележащему протоколу транспортного уровня и может использовать для передачи сообщений такие протоколы, как ICMP и IGMP

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

    Seqpacket : обеспечивает надежную двустороннюю передачу данных с установкой постоянного подключения

    Stream : обеспечивает надежную двустороннюю передачу данных с установкой постоянного подключения. Для связи используется протокол TCP, поэтому этот тип сокета используется в паре с типом протокола Tcp и значением AddressFamily.InterNetwork

    Unknown : адрес NetBios

    Для создания объекта сокета можно использовать один из его конструкторов. Например, сокет, использующий протокол Tcp:

    Или сокет, использующий протокол Udp:

    Таким образом, при создании сокета мы можем указывать разные комбинации протоколов, типов сокета, значений из перечисления AddressFamily. Однако в то же время не все комбинации являются корректными. Так, для работы через протокол Tcp, нам надо обязательно указать параметры: AddressFamily.InterNetwork, SocketType.Stream и ProtocolType.Tcp. Для Udp набор параметров будет другим: AddressFamily.InterNetwork, SocketType.Dgram и ProtocolType.Udp. Для других протоколов набор значений будет отличаться. Поэтому использование сокетов может потребовать некоторого знания принципов работы отдельных протоколов. Хотя в отношении Tcp и Udp все относительно просто.

    Общий принцип работы сокетов

    При работе с сокетами вне зависимости от выбранных протоколов мы будем опираться на методы класса Socket:

    Accept() : создает новый объект Socket для обработки входящего подключения

    Bind() : связывает объект Socket с локальной конечной точкой

    Close() : закрывает сокет

    Connect() : устанавливает соединение с удаленным хостом

    Listen() : начинает прослушивание входящих запросов

    Poll() : определяет состояние сокета

    Receive() : получает данные

    Send() : отправляет данные

    Shutdown() : блокирует на сокете прием и отправку данных

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

    При применении протокола, который требует установление соединения, например, TCP, сервер должен вызвать метод Bind для установки точки для прослушивания входящих подключений и затем запустить прослушивание подключений с помощью метода Listen. Далее с помощью метода Accept можно получить входящие запросы на подключение в виде объекта Socket, который используется для взаимодействия с удаленным узла. У полученного объекта Socket вызываются методы Send и Receive соответственно для отправки и получения данных. Если необходимо подключиться к серверу, то вызывается метод Connect. Для обмена данными с сервером также применяются методы Send или Receive.

    Если применяется протокол, для которого не требуется установление соединения, например, UDP, то после вызова метода Bind не надо вызывать метод Listen. И в этом случае для приема данных используется метод ReceiveFrom, а для отправки данных — метод SendTo.

    Сокеты Windows

    Winsock API разрабатывался как расширение Berkley Sockets API для среды Windows и поэтому поддерживается всеми системами Windows. К преимуществам Winsock можно отнести следующее:

    • Перенос уже имеющегося кода, написанного для Berkeley Sockets API, осуществляется непосредственно.

    • Системы Windows легко встраиваются в сети, использующие как версию IPv4 протокола TCP/IP, так и постепенно распространяющуюся версию IPv6. Помимо всего остального, версия IPv6 допускает использование более длинных IP-адресов, преодолевая существующий 4-байтовый адресный барьер версии IPv4.

    • Сокеты могут использоваться совместно с перекрывающимся вводом/выводом Windows (глава 14), что, помимо всего прочего, обеспечивает возможность масштабирования серверов при увеличении количества активных клиентов.

    • Сокеты можно рассматривать как дескрипторы (типа HANDLE) файлов при использовании функций ReadFile и WriteFile и, с некоторыми ограничениями, при использовании других функций, точно так же, как в качестве дескрипторов файлов сокеты применяются в UNIX. Эта возможность оказывается удобной в тех случаях, когда требуется использование асинхронного ввода/вывода и портов завершения ввода/вывода.

    • Существуют также дополнительные, непереносимые расширения.

    Похожие главы из других книг

    7.2.6.5. Сокеты

    7.2.6.5. Сокеты Сокеты (sockets) были разработаны в BSD-ветви Unix как способ инкапсуляции доступа к сетям данных. Две программы, осуществляющие обмен данными через сокет, обычно используют двунаправленный поток байтов (существуют и другие режимы сокетов и методы передачи, но они

    7.2.6.5. Сокеты

    7.2.6.5. Сокеты Сокеты (sockets) были разработаны в BSD-ветви Unix как способ инкапсуляции доступа к сетям данных. Две программы, осуществляющие обмен данными через сокет, обычно используют двунаправленный поток байтов (существуют и другие режимы сокетов и методы передачи, но они

    Сокеты Windows

    Сокеты Windows Winsock API разрабатывался как расширение Berkley Sockets API для среды Windows и поэтому поддерживается всеми системами Windows. К преимуществам Winsock можно отнести следующее:• Перенос уже имеющегося кода, написанного для Berkeley Sockets API, осуществляется непосредственно.• Системы

    Перекрывающиеся сокеты

    Перекрывающиеся сокеты Одним из наиболее важных нововведений в Windows Sockets 2.0 (глава 12) является стандартизация перекрывающегося ввода/вывода. В частности, сокеты уже не создаются автоматически как дескрипторы файлов с перекрытием. Функция socket создает неперекрывающийся

    2.1. Стандартные сокеты

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

    27.3.10. Сигналы и сокеты

    27.3.10. Сигналы и сокеты С сокетами связаны три сигнала:? SIGIO — сокет готов к вводу/выводу. Сигнал посылается процессу, который связан с сокетом;? SIGURG — сокет получил экспресс-данные (мы их использовать не будем, поэтому особо останавливаться на них нет смысла);? SIGPIPE — запись

    5.5. Сокеты

    5.5.4. Локальные сокеты

    5.5.4. Локальные сокеты Сокеты, соединяющие процессы в пределах одного компьютера, работают в локальном пространстве имен (PF_LOCAL или PF_UNIX, это синонимы). Такие сокеты называются локальными или UNIX-сокетами. Их адресами являются имена файлов, указываемые только при создании

    5.5.6. Internet-сокеты

    5.5.6. Internet-сокеты UNIX-сокеты используются для организации взаимодействия двух процессов, выполняющихся на одном компьютере. С другой стороны. Internet-сокеты позволяют соединять между собой процессы, работающие на разных компьютерах.Пространству имен Internet соответствует

    Сокеты

    Сокеты Сокеты предназначены для взаимодействия между процессами. Интерфейс сокетов часто используется для доступа к сети TCP/IP. В системах, ветви BSD UNIX на базе сокетов реализована система межпроцессного взаимодействия, с помощью которой работают многие системные сервисы,

    Часть 2 Элементарные сокеты

    Часть 2 Элементарные сокеты

    Глава 3 Введение в сокеты

    Глава 3 Введение в сокеты 3.1. Введение Эта глава начинается с описания программного интерфейса приложения (API) сокетов. Мы начнем со структур адресов сокетов, которые будут встречаться почти в каждом примере на протяжении всей книги. Эти структуры можно передавать в двух

    Сигнал SIGIO и сокеты UDP

    Сигнал SIGIO и сокеты UDP Использовать ввод-вывод, управляемый сигналом, с сокетами UDP довольно легко. Сигнал генерируется в следующих случаях:? на сокет прибывает дейтаграмма;? на сокете возникает асинхронная ошибка.Таким образом, когда мы перехватываем сигнал SIGIO для

    Сигнал SIGIO и сокеты TCP

    Сигнал SIGIO и сокеты TCP К сожалению, использовать управляемый сигналом ввод-вывод для сокетов TCP почти бесполезно. Проблема состоит в том, что сигнал генерируется слишком часто, а само по себе возникновение сигнала не позволяет выяснить, что произошло. Как отмечается в [128, с.

    Глава 28 Символьные сокеты

    Глава 28 Символьные сокеты 28.1. Введение Символьные, или неструктурированные, сокеты (raw sockets) обеспечивают три возможности, не предоставляемые обычными сокетами TCP и UDP.1. Символьные сокеты позволяют читать и записывать пакеты ICMPv4, IGMPv4 и ICMPv6. Например, программа ping посылает

    Приёмы сетевого программирования: сокетный движок

    Приветствую Вас, уважаемый читатель! Предоставляю Вашему суровому вниманию свою вторую статью, «Приёмы сетевого программирования: Сокетный движок». Надеюсь, чтение этой статьи будет для Вас не только полезным, но и приятным.

    Для людей, щепетильных в вопросах оригинальности и уникальности, вроде «хочу всегда только свежее!», следует отметить с самого начала, что тема, поднимаемая мной в этой статье — это далеко не новшество в сетевом программировании. Те или иные модели сокетных движков применяются в реализации прокси-серверов, сканеров сетей и в других программах, немноготредово реализующих обработку большого количества сокетов.

    Для понимания технических деталей этой статьи, потребуются:

    1. Базовый опыт в программировании на классическом ANSI C;
    2. Базовые знания стандрата posix.1b, в частности, системных вызовов BSD Sockets и файловых API UNIX и/или знания WinSock2 API под Windows (в идеале, и то, и другое);
    3. Небольшой опыт в программировании сетевых задач или в анализе кода сетевых программ.

    Надеюсь, пользователи UNIX не брезгуют программой man, а пользователи Windows всегда смогут найти руководство по WinSock2 в хелпах тех IDE, в которых они привыкли работать. Кроме того, в Интернете более чем достаточно учебников и по [1], по [2] и по [3].

    Сокетный движок: что это такое?

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

    Этот метод сугубо противоположен вызову отдельного процесса или потока выполнения для обработки каждого конкретного сокета. И именно в этом заключается основной его плюс (многократное понижение требований к ресурсам) и минус (многократное повышение требований к структурированию алгоритма программы).

    В качестве примера реализации самого простого сокетного движка я могу предложить
    исходный текст программы Grinder v1.1
    (49Kb), конкретно в файлах и . Здесь Вы можете взять бинарник этой программы под
    WinSock (40 Kb).

    Блокирующие вызовы сокетных API

    Прежде чем заняться моделированием сокетного движка, следует чётко уяснить разницу между блокирующими и неблокирующими вызовами сокетных API. Возьмём самый простой пример, в котором мы коннектимся к удалённому http-серверу и получаем список поддерживаемых им команд (пример для WinSock2):

    #include
    .
    SOCKET s;
    int rc;
    struct sockaddr_in dst;
    char buf[8192];
    .
    memset(buf,0,8129);
    memset(&dst,0,sizeof(struct sockaddr_in));
    dst.sin_addr.s_addr = inet_addr(«XXX.XXX.XXX.XXX»);
    dst.sin_port = htons(80);
    dst.sin_family = AF_INET;
    s = socket(AF_INET,SOCK_STREAM,0);
    rc = connect(s,(struct sockaddr*)&dst,sizeof(dst));
    if(rc == SOCKET_ERROR) <
    printf(«Error occured while connecting!\n»);
    return 1;
    >
    rc = send(s,»OPTIONS / HTTP/1.1\x0D\x0A\x0D\x0A»,22,0);
    if(rc

    Сценарий задачи описан линейно: мы просим у ядра сокет и коннектимся по структуре dst. В случае успеха, мы отправляем данные на сервер и, опять же, в случае успеха, принимаем данные с сервера.

    Вызовы connect() и recv() в данном примере являются блокирующими: они блокируют ход выполнения вызывающей их функции на время своего исполнения, которое может длиться неопределённое время. Вызов send() блокирующим не является: и в UNIX, и в Windows сокетный дескриптор представляет собой FIFO-структуру и send() лишь добавляет в очередь сокета необходимое количество данных, сразу же возвращая управление.

    Программы, построенные на так называемых «тредовых движках», вызывают отдельный поток выполнения для обработки каждого сокета и такого рода издержки слабо влияют на динамичность программы в целом. Однако, этот приём, мягко скажем, не очень уместен в том случае, когда нам необходимо обрабатывать большое количество сокетов (до нескольких тысяч): не открывать же для каждого сокета отдельный поток! :))) Даже несколько десятков потоков — это солидная и неоправданная нагрузка на ядро системы. Которую, впрочем, можно избежать.

    Неблокирующие вызовы сокетных API

    Когда мы открываем сокет в UNIX, ядро устанавливает ему дефолтный атрибут доступа: O_RDWR, «чтение/запись». С помощью файлого API fcntl() мы можем установить для открытого сокета неблокирующий режим работы, сохранив бит O_RDWR:

    .
    s = socket(AF_INET,SOCK_STREAM,0);
    if(s
    Следует отметить, что константа O_NONBLOCK в разных UNIX’ах может называться по-разному. Мне встречались такие: O_NDELAY, FNONBIO. Если Ваша система не знает O_NONBLOCK, загляните к себе в /usr/include/sys/fcntl.h.

    Таким образом, любая операция чтения или записи по дескриптору ‘s’ не будет блокировать ход выполнения вызывающей функции и будет моментально возвращать управление вызывающей функции. Для того, чтобы закрыть сокет, нам необходимо вернуть дескриптору ‘s’ его первоначальное состояние:

    WinSock (и первый, и второй), насколько это возможно в рамках ОС Windows, копирует названия и синтаксис API BSD Sockets. Принципиальное различие заключается в том, что API BSD Sockets «воспринимают» сокеты как файловые дескрипторы, к которым можно применять файловые API UNIX. В WinSock для сокетов выделен специальный тип данных, который обрабатывается особыми API. Открываем сокет и переводим его в неблокирующий режим:

    on = 1;
    WSADATA wsaData;
    .
    if (WSAStartup(MAKEWORD(2,1),&wsaData) != 0) <
    printf(«Bad WinSock!\n»);
    return 1;
    >
    .
    s = socket(AF_INET,SOCK_STREAM,0);
    if(s == SOCKET_ERROR) <
    printf(«Error allocating a socket!\n»);
    return 1;
    >
    ioctlsocket(s,FIONBIO,&on);
    .

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

    off = 0;
    .
    ioctlsocket(s,FIONBIO,&off);
    closesocket(s);
    WSACleanup();
    .

    Чтение из неблокирующего сокета и запись в него

    Как уже было отмечено ранее, вызовы connect() и recv() в неблокирующем режиме возвращают управление моментально, в не зависимости от результата выполнения и состояния сокета. Для того, чтобы алгоритм программы имел возможность адекватно и динамично реагировать на состояния сокетов, необходим инструмент контроля этих состояний.

    В стандарте posix.1 для UNIX, такой инструмент реализован в виде файлового API select() и макросов FD_*. WinSock2 полностью копирует вызов select() и макросы FD_*, за исключением того, что select() в WinSock2 фактически игнорирует значение первого аргумента, сообщающего функции select() количество сокетов, которые нужно обработать. Для обратной совместимости, всё же, имеет смысл всегда сообщать select()’у значение FD_SETSIZE для сохранения обратной совместимости, а компилятор уже подставит вместо FD_SETSIZE нужное значение.

    Рассмотрим следующий пример (действительный и для Windows, и для UNIX): мы имеем некую структуру сокетных дескрипторов int *s в количестве snum, уже открытых и обрабатываемых некой программой. Нам необходимо отобрать среди всех этих сокетов те, из которых мы имеем возможность произвести чтение в данный момент времени. И произвести эту запись.

    int i, rc;
    #ifdef POSIX
    int * s; // указатель на пачку сокетов для posix
    #else
    SOCKET * s; // указатель на пачку сокетов для Windows
    #endif
    int snum; // количество сокетов
    fd_set fdset; // дескриптор сокетов
    struct timeval tv = <0,0>; // 0 секунд, 0 микросекунд
    unsigned char * data_to_recv; // буфер
    .
    FD_ZERO(&fdset); // обнуляем дескриптор сокетов
    for(i=0;i

    Рассмотрим синтаксис вызова select() более подробно (взято из cygwin, /usr/include/sys/select.h):

    int select __P ((int __n, fd_set *__readfds, fd_set *__writefds, fd_set *__exceptfds, struct timeval *__timeout));

    int __n: количество сокетов, для обработки;
    fd_set *__readfds: указатель на дескриптор, в который надобно выставить сокеты, готовые к чтению;
    fd_set *__writefds: указатель на дескриптор, в который выставляются сокеты, годные к чтению;
    fd_set *__exceptfds: указатель на дескриптор, в который выставляются «порванные», ошибочные сокеты;
    struct timeval *__timeout: указатель на структуру timeval, где содержится количество времени, на которое select() задерживает возвращение управления вызывающей функции.

    Моделирование простого сокетного движка

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

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

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

    Рассмотрим моделирование сокетного движка под UNIX и под Windows на примере написанной мной программы. Это консольный сканер IP-диапазона на халявные анонимные SOCKSv5-прокси, Tiny Anonymous SOCKSv5 scanner. Пакет исходных текстов программы, в котором Вы найдёте обещанные мною в предыдущей статье подробные комментарии, Вы можете взять
    тут (10.5 Кб). Бинарник, скомпилированный под cygwin,
    в этом архиве, а бинарник под Windows
    тут.

    Итак, основной тип данных, с которым работает сокетный движок, это структура

    typedef struct <
    #ifdef SSCAN_WS2
    SOCKET s; // сокет в Windows
    #else
    int s; // сокет в posix
    #endif
    u32 job; // джоб, который сокет должен выполнить
    u32 timestamp; // отпечаток времени с последней успешно выполненной операции
    u32 addr; // адрес, с которым сокет, в данный момент, осуществляет соединение
    > s_st;

    В теле программы объявляется глобальный указатель

    s_st *s; // socket engine structure

    Сокетный движок обращается к структурам s_st, разыменовывая данный указатель.

    Джобов, обрабатываемых сокетным движком, в этой программе немного:

    1) J_CLOSED, сокет закрыт.
    2) J_CONNECTING, сокету дана команда connect().
    3) J_CONNECTED, connect() успешно завершён, можно записывать в сокет.
    4) J_SENT, сообщение в сокет отправлено.
    5) J_IDLE, обработка джоба завершена.

    Алгоритм в программе линейный:

    while(!enough) <
    1) последовательная обработка сокетов, связанных, в настоящий момент времени, с определённым джобом; переключение джоба в случае успешного прохождения всех необходимых операций, в противном случае s[i].job устанавливается в J_CLOSED;
    2) проверка таймаутов сокетов, джобы которых находятся в состоянии J_CONNECTING и J_SENT;
    3) проверка ошибок в tcp-соединении;
    4) запись файла-отпечатка прогресса работы сканера;
    5) поиск ответа на вопрос: «а не пора ли закругляться?» ��
    6) задержка движка на время, определённое пользователем.
    >

    При реализации сокетного движка и под UNIX, и под Windows, крайне важно перехватывать и контролировать сигналы SIGINT (UNIX+Windows), SIGPIPE и SIGQUIT в UNIX, SIGBREAK и SIGTERM в Windows.

    Я игнорирую сигнал SIGPIPE в UNIX, который отправляется в мой процесс в том случае, если я пытаюсь произвести какую-то, с точки зрения ядра, недопустимую операцию с сокетом. Если этот сигнал не перехватывать и не игнорировать, программа будет завершаться с ошибкой «Broken Pipe». Я лично не вижу никакого смысла обрабатывать эту ситуацию в рамках моей программы.

    Сигналы SIGINT и SIGQUIT в UNIX, сигналы SIGBREAK и SIGTERM в Windows отвечают за прерывание потока выполнения программы пользователем. Если программа завершается подобным образом, необходимо «опустить» в блокирующий режим и закрыть все сокеты, открытые программой. Не знаю, как для UNIX (который, вроде бы, закрывает все файловые дескрипторы, открытые потоком выполнения при завершении этого потока), но для Windows это критично!

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

    Пример работы с сокетами для window

    Я люблю учить на примерах, так что как насчет программы, использующей TCP/IP, которая подключается к Интернету, посылает HTTP-запрос на Web-сервер и отображает главную страницу сайта? Перед тем, как я перейду к коду, посмотрите на рис. 14.10, где показано что именно мы будем делать.

    Рис. 14.10. Ход выполнения простой программы, использующей сокеты

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

    Программа Sockets_Receive

    Я реализовал код, необходимый для воссоздания этапов, изображенных на рис. 14.10. Загрузите программу Sockets_Receive и следуйте за мной. Проект состоит из файла main.cpp и единственной библиотеки ws2_32.lib , которая содержит все, что необходимо для программирования сокетов в Windows. Скомпилируйте программу и запустите ее. Вы увидите окно консольного приложения, в котором отображается содержимое главной страницы сайта, имя которого задано в коде. Как это должно выглядеть, показано на рис. 14.11.

    Рис. 14.11. Окно программы Sockets_Receive

    Открыв файл main.cpp вы увидите следующий код:

    Включение заголовочного файла WinSock

    Заголовочный файл winsock.h содержит всю необходимую информацию для работы с библиотекой сокетов ws2_32.lib . Убедитесь, что включаете его в любой ваш код, который работает с сокетами. Остальные заголовочные файлы применяются ежедневно в обычном программировании.

    Установка версии WinSock

    Перед тем, как вы вообще сможете использовать какие-либо сокеты, необходимо инициализировать систему сокетов Windows, вызвав функцию WSAStartup(). Эта функция получает номер версии сокетов, которую вы намереваетесь использовать и инициализирует коммуникационную систему. Раз вы хотите использовать сокеты версии 2, установите номер запрашиваемой версии равным 2.

    Создание сокета

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

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

    Поиск сервера по URL

    Если вы хотите установить соединение с сервером, используя 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

    Программа 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 не требуется для работы с сетью, я использую ее для воспроизведения звукового сигнала, когда пользователь щелкает по кнопке завершения хода.

    Глобальные переменные программы Sockets_TurnGame

    Загрузите заголовочный файл main.h , и вы увидите в нем такой код:

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

    Логическое значение g_bIsServer сообщает вам работает ли программа в режиме ведущего компьютера. Если значение равно 1, значит программа является сервером и должна ожидать подключения клиента. Если значение равно 0, программа является клиентом и должна установить соединение с ведущим игровым компьютером.

    Логическое значение g_bMyTurn сообщает принадлежит ли вам в данный момент право хода в игре. Если сейчас ваш ход, будет отображаться кнопка Turn Done, щелкнув по которой вы передадите ход другому игроку. Если сейчас ваш ход, значение переменной равно 1, если нет — значение переменной равно 0.

    Логическое значение g_bConnected сообщает вам установила ли программа соединение с другим игроком. 1 означает что соединение существует, 0 — что нет.

    Есть и еще несколько глобальных переменных, но они относятся к элементам управления Windows и другим подобным вещам.

    Функции программы Sockets_TurnGame

    Далее в заголовочном файле main.h приведены прототипы используемых в программе функций. Вот они:

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

    Функция vInitializeSockets() используется для начальной инициализации WinSock.

    Функция vShutdownSockets() отключает все активные соединения и систему WinSock.

    Функция vConnect() вызывается когда пользователь щелкает по кнопке Connect. Она пытается подключиться к ведущему компьютеру, чей IP-адрес указан в окне. После того, как соединение установлено, клиент получает контроль над игрой и может закончить ход в выбранный им момент времени.

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

    Функция vReceiveTurnMessage() ждет, пока другой игрок не пришлет сообщающий о завершении хода пакет. Функция будет сидеть и ждать, пока пока рак на горе не свистнет.

    Функция vTurnDone() вызывается функциями отправки и приема хода для завершения хода. Это происходит когда пользователь щелкает по кнопке Turn Done.

    Остальные перечисленные в заголовочном файле main.h функции являются стандартным каркасом приложения для Windows и не слишком интересны, поэтому я не буду их рассматривать. Вы же не хотите, чтобы я по сто раз описывал одно и то же? Лучше я лишний раз сыграю в Age of Mythology!

    Функция vHost()

    Представьте на минуту, что вы загрузили программу походовой игры и щелкнули по кнопке Host. Начнет выполняться следующий код:

    Первая часть кода реализует логику для подключения клиента. Фактически программа прослушивает порт, ожидая соединения и принимает соединение, когда оно происходит. После этого код убирает кнопки Host и Connect, чтобы пользователь не мог щелкнуть по ним еще раз. Затем программа устанавливает переменную хода, чтобы она указывала, что контроль над игрой находится у клиента. И, наконец, ход заканчивается вызовом функции завершения хода. Это переводит сервер в режим приема, чтобы он мог получить сообщение о завершении хода от клиента. Все эти действия показаны на рис. 14.16.

    Рис. 14.16. Ход выполнения функции vHost()

    Функция vConnect()

    Функция vConnect() вызывается, когда игрок щелкает по кнопке Connect. Вот как выглядит ее код:

    Код подключения очень похож на тот, который я показывал вам в примере подключения к веб-серверу. Клиент сперва получает адрес сервера, а затем пытается установить соединение с ним. Как только соединение успешно установлено, клиент убирает кнопки Connect и Host и отображает новую кнопку Turn Done. В этот момент клиенту дается время, чтобы он мог сделать свой ход. Ход выполнения функции показан на рис. 14.17.

    Рис. 14.17. Ход выполнения функции vConnect()

    Функция vTurnDone()

    Функция завершения хода выполняет две различных задачи. Если сейчас ваш ход, она отправляет сообщение о завершении хода другому игроку и ждет получения сообщения. Если сейчас ход другого игрока, функция ждет, пока он не завершит свой ход. Ход выполнения функции показан на рис. 14.18.

    Рис. 14.18. Ход выполнения функции vTurnDone()

    Хотя блок-схема и выглядит запутанной, сам код не слишком сложен. Большая его часть занимается обработкой сообщений от окна. Если ее отбросить, оставшийся код будет выглядеть примерно так:

    Если вы сравните приведенный выше код с тем, который находится в файле main.cpp , то увидите что здесь код значительно короче. Я удалил из него текстовые сообщения, чтобы вам проще было увидеть, что происходит.

    Функция vSendTurnMessage()

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

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

    Функция vReceiveTurnMessage()

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

    В функции приема я вызываю функцию recv для приема пакета от другого игрока. Как только пакет пришел, код устанавливает флаг хода и создает кнопку Turn Done. Вот и все об отправке и получении пакетов!

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

    Илон Маск рекомендует:  Что такое код yp_cat
    Понравилась статья? Поделиться с друзьями:
    Кодинг, CSS и SQL