- Отправка Push-уведомлений
- Отправка уведомлений по электронной почте
- Отправка файлов на сервер FTP
- Обмен данными с веб-сервером по протоколу HTTP/HTTPS
- Установление и разрыв соединения сетевого сокета
- Проверка состояния сокета
- Настройка таймаутов передачи и приема данных сокетами
- Чтение, запись данных по незащищенному сокет-соединению
- Подготовка защищенного сокет-соединения
- Чтение и запись данных по защищенному сокет-соединению
Чтение и запись данных по защищенному сокет-соединению
Для защищенного соединения имеется свой набор функций обмена данными между клиентом и сервером. Названия и принцип работы функций почти совпадают с уже рассмотренными функциями SocketRead и SocketSend.
int SocketTlsRead(int socket, uchar &buffer[], uint maxlen)
Функция SocketTlsRead читает данные из защищенного TLS-соединения, открытого на указанном сокете. Данные попадают в передаваемый по ссылке массив buffer. Если он динамический, его размер будет увеличен под объем данных, но не более чем до INT_MAX (2147483647) байтов.
Параметр maxlen задает количество расшифрованных байтов, которые требуется получить (их количество всегда меньше, чем объем "сырых" зашифрованных данных, поступающих во внутренний буфер сокета). Данные, которые не поместятся в массив, останутся в сокете, и их можно будет получить следующим вызовом SocketTlsRead.
Функция исполняется до тех пор, пока не получит указанное количество данных или не наступит таймаут, заданный с помощью SocketTimeouts.
В случае успеха функция возвращает количество прочитанных байтов, в случае ошибки — -1, при этом в _LastError записывается код 5273 (ERR_NETSOCKET_IO_ERROR). Наличие ошибки говорит о том, что соединение было разорвано.
int SocketTlsReadAvailable(int socket, uchar &buffer[], const uint maxlen)
Функция SocketTlsReadAvailable читает все доступные расшифрованные данные из защищенного TLS-соединения, но не более maxlen байтов. В отличие от SocketTlsRead, SocketTlsReadAvailable не ждет обязательного наличия заданного количества данных и сразу возвращает только то, что есть. Таким образом, если внутренний буфер сокета "пуст" (с сервера пока ничего не поступило, уже было прочитано или еще не сформировало готовый для дешифрации блок), функция вернет 0, и в приемный массив buffer ничего не будет записано. Это — штатная ситуация.
Величина maxlen должна быть в пределах от 1 до INT_MAX (2147483647).
int SocketTlsSend(int socket, const uchar &buffer[], uint bufferlen)
Функция SocketTlsSend отправляет данные из массива buffer через защищенное соединение, открытое на указанном сокете. Принцип действия тот же, что у описанной ранее функции SocketSend — разница только в типе соединения.
За основу нового примера скрипта SocketReadWriteHTTPS.mq5 возьмем предыдущий SocketReadWriteHTTP.mq5, но добавим гибкости — выбор HTTP-метода (по умолчанию GET, а не HEAD), настройку таймаута и поддержку защищенных соединений. Стандартным таким портом является 443.
input string Method = "GET"; // Method (HEAD,GET)
|
Сервером по умолчанию указан "www.google.com" — не забудьте добавить его (и любой другой, который вы введете) в список разрешенных в настройках терминала.
Определять, является ли соединение защищенным или нет, будем с помощью функции SocketTlsCertificate: если она завершится успешно, значит сервер предоставил сертификат, и режим TLS активен. Если функция вернет false и взведет код ошибки NETSOCKET_NO_CERTIFICATE(5275) — значит, мы пользуемся обычным соединением, но ошибку можно проигнорировать и сбросить, поскольку нас устраивает и незащищенное соединение.
void OnStart()
|
Остальная часть функции OnStart выполнена по прежнему плану: отправить запрос с помощью функции HTTPSend и принять ответ с помощью HTTPRecv. Но на этот раз мы дополнительно передаем в эти функции признак TLS, и они должны быть реализованы слегка иначе.
if(PRTF(HTTPSend(socket, StringFormat("%s / HTTP/1.1\r\nHost: %s\r\n"
|
На примере HTTPSend легко увидеть, что теперь в зависимости от флага TLS, мы используем либо SocketTlsSend, либо SocketSend.
bool HTTPSend(int socket, const string request, const bool TLS)
|
С функцией HTTPRecv все несколько сложнее. Поскольку мы предоставляем возможность скачивать всю страницу (а не только заголовки), требуется неким образом узнать, получили ли мы все данные. Даже после передачи всего документа сокет, как правило, остается открытым для оптимизации последующих предполагаемых запросов. Но наша программа не будет знать, прекратилась ли передача штатно или, может быть, случился временный "затор" где-то в сетевой инфраструктуре (подобную неспешную, прерывистую загрузку страниц иногда можно наблюдать и в браузерах). Или наоборот, в случае обрыва соединения мы можем ошибочно посчитать, что приняли весь документ.
Дело в том, что сокеты сами по себе выступают лишь средством коммуникации программ и работают с абстрактными блоками данных: им неизвестно, какого типа данные, что они означают, и где у них логическое завершение. Всеми этими вопросами занимаются прикладные протоколы вроде HTTP. Поэтому нам потребуется углубиться в спецификацию и реализовать проверки самостоятельно.
bool HTTPRecv(int socket, string &result, const uint timeout, const bool TLS)
|
Наиболее простой метод определения размера принимаемых данных основывается на анализе заголовка "Content-Length:". Для него нам потребовались переменные lastLF, size, content_length. Правда, заголовок этот присутствует не всегда, и в дело вступают "чанки" (chunks) — для их обнаружения введены переменные chunk_size, chunk_start, crlf и crlf_length.
Для демонстрации различных техник приема данных воспользуемся в этом примере "неблокирующей" функцией SocketTlsReadAvailable. Однако, аналогичная функция для незащищенного соединения отсутствует, и потому нам предстоит её написать самим (чуть позже). Общая схема алгоритма проста — это цикл с попытками получения новых блоков данных размером 1024 (или меньше) байтов. Если что-то удалось прочитать, мы это аккумулируем в массиве response. Если входной буфер сокета пуст, функции вернут 0, и мы делаем маленькую паузу. Наконец, если возникнет ошибка или таймаут, цикл прервется.
uint start = GetTickCount();
|
Прежде всего, необходимо дождаться во входном потоке данных завершения HTTP-заголовока. Как мы уже видели из предыдущего примера, заголовки отделены от документа двойным переводом строки, то есть последовательностью символов "\r\n\r\n". Её легко обнаружить по двум символам '\n' (LF), расположенным через один.
Результатом поиска станет смещение в байтах от начала данных, где кончается заголовок и начинается документ. Мы сохраним его в переменную body.
if(body == 0) // ищем завершение заголовков, пока не найдем
|
При этом сразу выполняется поиск заголовка "Content-Length:" и извлечение из него размера. Заполненная переменная size дает возможность написать дополнительный условный оператор для выхода из цикла приема данных, когда получен весь документ.
Некоторые сервера "отдают" содержимое по частям — именно они называются "chunks". В таких случаях в HTTP-заголовке присутствует строка "Transfer-Encoding: chunked", а строка "Content-Length:" отсутствует. Каждый "чанк" начинается с шестнадцатеричного числа, указывающего размер "чанка", после чего следует перевод строки и указанное количество байтов с данными. Завершается "чанк" еще одним переводом строки. Последний "чанк", обозначающий конец документа, имеет нулевой размер.
Обратите внимание, что деление на подобные сегменты выполняется сервером, исходя из собственных, текущих "предпочтений" по оптимизации отправки, и никак не связано с блоками (пакетами) данных, на которые информация делится на уровне сокетов для передачи по сети. Иными словами, "чанки" сами, как правило, произвольным образом фрагментированы, и граница между сетевыми пакетами может случиться даже между цифр в размере "чанка".
Схематично это можно изобразить так (слева "чанки" документа, справа блоки данных из сокет-буфера).
Фрагментация веб-документа при передаче на уровнях HTTP и TCP
В нашем алгоритме пакеты — это то, что попадает на каждой итерации в массив block, но анализировать их по одиночке не имеет смысла и вся основная работа идет с общим массивом response.
Итак, если HTTP-заголовок полностью получен, но в нем не обнаружена строка "Content-Length:", мы переходим в ветвь алгоритма с режимом "Transfer-Encoding: chunked". По текущей позиции body в массиве response (сразу после завершения HTTP-заголовков) выделяется фрагмент строки и преобразуется в число из предположения шестнадцатеричного формата: это делает вспомогательная функция HexStringToInteger (см. прилагаемый исходный код). Если там действительно обнаруживается число, мы записываем его в chunk_size, помечаем позицию, как начало "чанка" в chunk_start и удаляем из response байты с самим числом и обрамляющими переводами строк.
...
|
Теперь для проверки завершенности документа нужно анализировать не только переменную size (которая, как мы видели, может быть фактически выключена из работы присвоением -1 в отсутствии "Content-Length:"), но и новые переменные для "чанков": chunk_start и chunk_size. Принцип действий тот же самый, что и после HTTP-заголовков: по смещению в массиве response, где завершился предыдущий "чанк", вычленяем размер следующего "чанка". Продолжаем процесс, пока не найдем "чанк" нулевого размера.
...
|
Таким образом, мы обеспечили выход из цикла по результатам анализа входящего потока двумя разными способами (помимо выхода по таймауту и по ошибке). При штатном завершении цикла мы преобразуем в строку ту часть массива response, которая начинается с позиции body и содержит целый документ, а иначе вернем просто все, что успели получить, вместе с заголовками — для "разбора полетов".
bool HTTPRecv(int socket, string &result, const uint timeout, const bool TLS)
|
Осталось показать функцию SocketReadAvailable — аналог SocketTlsReadAvailable для незащищенных соединений.
int SocketReadAvailable(int socket, uchar &block[], const uint maxlen = INT_MAX)
|
Скрипт готов к работе.
От нас потребовалось довольно много усилий, чтобы реализовать простой запрос веб-страницы с помощью сокетов. Это служит демонстрацией того, какая большая рутинная работа обычно скрывается в поддержке сетевых протоколов на низком уровне. Конечно, в случае HTTP нам проще и правильнее использовать встроенную реализацию WebRequest, но она не включает всех возможностей HTTP (причем мы вскользь затронули HTTP 1.1, а есть еще HTTP/2), да и количество других прикладных протоколов огромно, поэтому для их интеграции в MetaTrader 5 не обойтись без Socket-функций.
Запустим SocketReadWriteHTTPS.mq5 с параметрами по умолчанию.
Server=www.google.com / ok
|
Как мы видим, документ передается "чанками" и был сохранен во временный файл (вы можете найти его в MQL5/Files/www.mql5.com.htm).
Запустим теперь скрипт для сайта "www.mql5.com" и порта 80. Из предыдущего раздела мы знаем, что сайт в этом случае выдает перенаправление на свою защищенную версию, но этот "редирект" не пустой: у него есть документ-заглушка, и теперь мы можем получить его полностью. Для нас здесь важно, что в данном случае корректно используется заголовок "Content-Length:".
Server=www.mql5.com / ok
|
Еще один, большой пример использования сокетов на практике мы рассмотрим в главе Проекты.