- Отправка Push-уведомлений
- Отправка уведомлений по электронной почте
- Отправка файлов на сервер FTP
- Обмен данными с веб-сервером по протоколу HTTP/HTTPS
- Установление и разрыв соединения сетевого сокета
- Проверка состояния сокета
- Настройка таймаутов передачи и приема данных сокетами
- Чтение, запись данных по незащищенному сокет-соединению
- Подготовка защищенного сокет-соединения
- Чтение и запись данных по защищенному сокет-соединению
Чтение, запись данных по незащищенному сокет-соединению
Исторически так сложилось, что сокеты по-умолчанию обеспечивают передачу данных по простому соединению — в открытом виде, и это позволяет техническими средствами анализировать весь трафик. В последние годы к вопросам безопасности стали относиться более серьезно и потому практически повсеместно внедрена технология TLS (Transport Layer Security): она обеспечивает шифрование на лету всех данных между отправителем и получателем. В частности, для интернет-соединений разница заключается в протоколах HTTP (простое соединение) и HTTPS (защищенное).
В MQL5 существуют разные наборы Socket-функций для работы с простыми и защищенными соединениями. В этом разделе мы познакомимся с простым режимом, а позднее доберемся и до защищенного.
Для чтения данных из сокета используется функция SocketRead.
int SocketRead(int socket, uchar &buffer[], uint maxlen, uint timeout)
В функцию передается дескриптор сокета, полученный из SocketCreate и подключенный к сетевому ресурсу с помощью SocketConnect.
Параметр buffer представляет собой ссылку на массив, в который будут прочитаны данные. Если массив динамический, его размер увеличивается на количество прочитанных байт, но он не может превышать INT_MAX (2147483647). Ограничить количество читаемых байтов можно параметром maxlen. Данные, которые не поместятся, останутся во внутреннем буфере сокета: их можно будет получить следующим вызовом SocketRead. Величина maxlen должна быть в пределах от 1 до INT_MAX (2147483647).
Параметр timeout задает время (в миллисекундах) ожидания завершения чтения. Если в течение этого времени не удается получить данные, попытки завершаются, и функция завершает работу: результат при этом равен -1.
-1 также возвращается и при ошибке, а её код в _LastError — такой как 5273 (ERR_NETSOCKET_IO_ERROR), например, означает, что соединение, установленное через SocketConnect, разорвано.
В случае успеха функция возвращает количество прочитанных байтов.
При выставлении таймаута чтения в 0, используется значение по умолчанию 120000 (2 минуты).
Для записи данных в сокет используется функция SocketSend.
К сожалению, названия функций SocketRead и SocketSend не являются "симметричными": обратной по смыслу операцией для "read" является "write", а для "send" — "receive". Это может оказаться непривычным для разработчиков со стажем, знакомым с сетевыми API на других платформах.
int SocketSend(int socket, const uchar &buffer[], uint maxlen)
В первом параметре передается дескриптор ранее созданного и открытого сокета. При передаче неверного хэндла в _LastError записывается ошибка 5270 (ERR_NETSOCKET_INVALIDHANDLE). В массиве buffer содержатся данные для отправки, их размер — в параметре maxlen (параметр введен для удобства отправки части данных из фиксированного массива).
В случае успеха функция возвращает количество байт, записанных в сокет, а в случае ошибки — -1.
Ошибки системного уровня (5273, ERR_NETSOCKET_IO_ERROR) сигнализируют о разрыве соединения.
В скрипте SocketReadWriteHTTP.mq5 продемонстрируем, как с помощью сокетов можно реализовать работу по протоколу HTTP, то есть запросить информацию о странице с веб-сервера. Это малая часть того, что "за сценой" для нас делает функция WebRequest.
Оставим во входных параметрах адрес по умолчанию — сайт "www.mql5.com". Номер порта выбран равным 80, поскольку это значение по умолчанию для незащищенных HTTP-соединений (хотя некоторые сервера могут использовать и другой порт: 81, 8080 и т.д.). Порты, зарезервированные под защищенные соединения (в частности, самый популярный 443), данным примером пока не поддерживаются. Также в параметре Server важно вводить именно название домена, а не конкретной страницы, потому что скрипт умеет запрашивать только главную страницу, то есть корневой путь "/".
input string Server = "www.mql5.com";
|
В главной функции скрипта создадим сокет и откроем на нем соединение с указанными параметрами (таймаут равен 5 секундам).
void OnStart()
|
Далее вспомним принцип работы протокола HTTP. Клиент отправляет запросы в виде специально оформленных заголовков (строк с предопределенными названиями и значениями), включающими, в частности, адрес веб-страницы, а сервер в ответ присылает всю веб-страницу или статус операции, используя для этого также специальные заголовки. Клиент может запросить веб-страницу с помощью GET-запроса, отправить некие данные с помощью POST-запроса или проверить статус веб-страницы с помощью экономного HEAD-запроса. В принципе, HTTP-методов гораздо больше — с ними можно познакомиться в спецификации протокола HTTP.
Таким образом, скрипт должен сформировать и отправить через сокет-соединение HTTP-заголовок. В простейшем виде получить мета-информацию о странице позволяет следующий HEAD-запрос (мы могли бы заменить HEAD на GET, чтобы запросить страницу целиком, но там есть некоторые сложности — об этом позднее).
HEAD / HTTP/1.1
|
Косая наклонная черта после "HEAD" (или другого метода) — это кратчайший возможный путь на любом сервере — корневой каталог, который обычно приводит к показу главной страницы. Если нам нужна конкретная веб-страница, мы могли бы написать что-то вроде "GET /en/forum/ HTTP/1.1" и получили бы оглавление англоязычных форумов на mql5.com. Вместо строки "_server_" должен быть подставлен реальный домен.
Наличие "User-Agent:" не обязательно, но позволяет программе "представиться" серверу, без чего некоторые сервера могут отклонить запрос.
Обратите внимание, на две пустые строки: они обозначают конец заголовка. В нашем скрипте заголовок удобно формировать таким выражением:
StringFormat("HEAD / HTTP/1.1\r\nHost: %s\r\n\r\n", Server) |
Осталось отправить его на сервер. Для этой цели написана простая функция HTTPSend. Она принимает дескриптор сокета и строку заголовка.
bool HTTPSend(int socket, const string request)
|
Внутри мы преобразуем строку в массив байтов и вызываем SocketSend.
Далее нам потребуется принять ответ сервера, для чего написана функция HTTPRecv. Она также ожидает дескриптор сокета и ссылку на строку, куда следует поместить данные, но устроена сложнее.
bool HTTPRecv(int socket, string &result, const uint timeout)
|
Здесь мы в цикле проверяем появление данных в пределах заданного таймаута и читаем их в буфер response. Возникновение ошибки прерывает цикл.
Байты буфера сразу же конвертируются в строку и стыкуются в полный ответ в переменной result. Важно отметить, что функцию CharArrayToString с кодировкой по умолчанию мы можем использовать только для HTTP-заголовка, потому что в нем разрешены только латинские буквы и несколько специальных символов из ANSI.
Для приема полного веб-документа, который, как правило, имеет кодировку UTF-8 (но потенциально может иметь и другую — нелатинскую, которая указывается как раз в HTTP-заголовке) потребуется более хитрая обработка: сначала нужно собрать все присланные блоки в одном общем буфере, а потом всё целиком преобразовывать в строку с указанием CP_UTF8 (в противном случае, любой символ, закодированный двумя байтами, может быть "разрезан" при отправке и прибудет к нам в разных блоках — именно поэтому нельзя ожидать корректного потока байт UTF-8 в отдельно взятом фрагменте). Мы улучшим данный пример в следующих разделах.
Имея функции HTTPSend и HTTPRecv, завершим код OnStart.
void OnStart()
|
В получаемом от сервера HTTP-заголовке интерес могут представлять следующие строки:
- 'Content-Length:' — общая длина документа в байтах;
- 'Content-Language:' — язык документа (например, "de-DE, ru");
- 'Content-Type:' — кодировка документа (например, "text/html; charset=UTF-8");
- 'Last-Modified:' — время последнего изменения документа, чтобы не скачивать то, что уже есть (в принципе, мы для этого можем в своем HTTP-запросе добавить заголовок 'If-Modified-Since:').
Про выяснение длины документа (размера данных) мы еще поговорим более подробно, потому что практически все заголовки являются опциональными, то есть сообщаются сервером по желанию, и при их отсутствии используются альтернативные механизмы. Размер важен, чтобы знать, когда закрывать соединение, то есть убедиться, что получены все данные.
Запуск скрипта с параметрами по умолчанию выдает такой результат.
Server=www.mql5.com / ok
|
Обратите внимание, что данный сайт, как и большинство сегодня, перенаправляет наш запрос на защищенное соединение: это достигается кодом статуса "301 Moved Permanently" и новым адресом "Location: https://www.mql5.com/" (здесь важен протокол "https"). Чтобы повторить запрос с поддержкой TLS, следует использовать несколько других функций, о которых и пойдет далее речь.