Trao đổi dữ liệu với máy chủ web thông qua HTTP/HTTPS
MQL5 cho phép bạn tích hợp các chương trình với các dịch vụ web và yêu cầu dữ liệu từ Internet. Dữ liệu có thể được gửi và nhận qua giao thức HTTP/HTTPS bằng hàm WebRequest
, hàm này có hai phiên bản: một phiên bản đơn giản hóa và một phiên bản nâng cao để tương tác với các máy chủ web.
int WebRequest(const string method, const string url, const string cookie, const string referer,
int timeout, const char &data[], int size, char &result[], string &response)
int WebRequest(const string method, const string url, const string headers, int timeout,
const char &data[], char &result[], string &response)
Sự khác biệt chính giữa hai hàm là phiên bản đơn giản hóa chỉ cho phép bạn chỉ định hai loại tiêu đề trong yêu cầu: cookie
và referer
, tức là địa chỉ mà từ đó chuyển hướng được thực hiện (không có lỗi đánh máy ở đây — theo lịch sử, từ "referrer" được viết trong tiêu đề HTTP với một chữ 'r'). Phiên bản mở rộng nhận tham số headers
tổng quát để gửi một tập hợp tiêu đề tùy ý. Tiêu đề yêu cầu có dạng "tên: giá trị" và được nối bằng ký tự xuống dòng "\r\n" nếu có nhiều hơn một tiêu đề.
Nếu giả định rằng chuỗi cookie
phải chứa "name1=value1; name2=value2" và liên kết referer
là "google.com", thì để gọi phiên bản thứ hai của hàm với hiệu quả tương tự như phiên bản đầu tiên, chúng ta cần thêm vào tham số headers
: "Cookie: name1=value1; name2=value2\r\nReferer: google.com".
Tham số method
chỉ định một trong các phương thức giao thức, "HEAD", "GET" hoặc "POST". Địa chỉ của tài nguyên hoặc dịch vụ được yêu cầu được truyền trong tham số url
. Theo đặc tả HTTP, độ dài của định danh tài nguyên mạng bị giới hạn ở 2048 byte, nhưng tại thời điểm viết sách, MQL5 có giới hạn là 1024 byte.
Thời gian tối đa của một yêu cầu được xác định bởi timeout
tính bằng mili giây.
Cả hai phiên bản của hàm đều truyền dữ liệu từ mảng data
đến máy chủ. Phiên bản đầu tiên còn yêu cầu chỉ định kích thước của mảng này tính bằng byte (size
).
Để gửi các yêu cầu đơn giản với giá trị của một vài biến, bạn có thể kết hợp chúng thành một chuỗi như "name1=value1&name2=value2&..." và thêm chúng vào địa chỉ yêu cầu GET, sau ký tự phân cách '?' hoặc đặt vào mảng data
cho yêu cầu POST với tiêu đề "Content-Type: application/x-www-form-urlencoded". Đối với các trường hợp phức tạp hơn, như tải tệp lên, sử dụng yêu cầu POST và "Content-Type: multipart/form-data".
Mảng nhận result
chứa nội dung phản hồi của máy chủ (nếu có). Các tiêu đề phản hồi của máy chủ được đặt trong chuỗi response
.
Hàm trả về mã phản hồi HTTP của máy chủ hoặc -1 trong trường hợp xảy ra lỗi hệ thống (ví dụ, vấn đề liên lạc hoặc lỗi tham số). Các mã lỗi tiềm năng có thể xuất hiện trong _LastError
bao gồm:
- 5200 — ERR_WEBREQUEST_INVALID_ADDRESS — URL không hợp lệ
- 5201 — ERR_WEBREQUEST_CONNECT_FAILED — không thể kết nối đến URL được chỉ định
- 5202 — ERR_WEBREQUEST_TIMEOUT — vượt quá thời gian chờ để nhận phản hồi từ máy chủ
- 5203 — ERR_WEBREQUEST_REQUEST_FAILED — bất kỳ lỗi nào khác do kết quả của yêu cầu
Hãy nhớ rằng ngay cả khi yêu cầu được thực thi mà không có lỗi ở cấp độ MQL5, một lỗi ứng dụng có thể nằm trong mã phản hồi HTTP của máy chủ (ví dụ, yêu cầu xác thực, định dạng dữ liệu không hợp lệ, trang không tìm thấy, v.v.). Trong trường hợp này, kết quả sẽ trống và hướng dẫn giải quyết tình huống thường được làm rõ bằng cách phân tích các tiêu đề response
nhận được.
Để sử dụng hàm WebRequest
, các địa chỉ máy chủ cần được thêm vào danh sách URL được phép trong tab Expert Advisors
trong cài đặt terminal. Cổng máy chủ được tự động chọn dựa trên giao thức được chỉ định: 80 cho "http://" và 443 cho "https://".
Hàm
WebRequest
là đồng bộ, tức là nó tạm dừng thực thi chương trình trong khi chờ phản hồi từ máy chủ. Do đó, hàm này không được phép gọi từ các chỉ báo, vì chúng hoạt động trong các luồng chung cho mỗi ký tự. Sự chậm trễ trong việc thực thi một chỉ báo sẽ dừng cập nhật tất cả các biểu đồ cho ký hiệu này.
Khi làm việc trong trình kiểm tra chiến lược, hàm WebRequest
không được thực thi.
Hãy bắt đầu với một script đơn giản WebRequestTest.mq5
thực hiện một yêu cầu duy nhất. Trong các tham số đầu vào, chúng ta sẽ cung cấp lựa chọn cho phương thức (mặc định là "GET"), địa chỉ của trang web thử nghiệm, các tiêu đề bổ sung (tùy chọn) và thời gian chờ.
input string Method = "GET"; // Method (GET,POST)
input string Address = "https://httpbin.org/headers";
input string Headers;
input int Timeout = 5000;
2
3
4
Địa chỉ được nhập như trong dòng trình duyệt: tất cả các ký tự bị cấm bởi đặc tả HTTP để sử dụng trực tiếp trong địa chỉ (bao gồm cả ký tự bảng chữ cái địa phương) được hàm WebRequest
tự động "che" trước khi gửi theo thuật toán urlencode
(trình duyệt cũng làm điều tương tự, nhưng chúng ta không thấy điều đó, vì dạng này được thiết kế để truyền qua cơ sở hạ tầng mạng, không phải cho con người).
Chúng ta cũng sẽ thêm tùy chọn DumpDataToFiles
: khi nó bằng true
, script sẽ lưu phản hồi của máy chủ vào một tệp riêng vì nó có thể khá lớn. Giá trị false
chỉ thị xuất dữ liệu trực tiếp vào nhật ký.
input bool DumpDataToFiles = true;
Chúng ta phải nói ngay rằng việc kiểm tra các script như vậy yêu cầu một máy chủ. Những người quan tâm có thể cài đặt một máy chủ web cục bộ, ví dụ, node.js, nhưng điều này đòi hỏi phải tự chuẩn bị hoặc cài đặt các script phía máy chủ (trong trường hợp này, kết nối các mô-đun JavaScript). Cách dễ hơn là sử dụng các máy chủ web thử nghiệm công khai có sẵn trên Internet. Bạn có thể sử dụng, ví dụ,
httpbin.org
,httpbingo.org
,webhook site
,putsreq.com
,www.mockable.io
, hoặcreqbin.com
. Chúng cung cấp các tập hợp tính năng khác nhau. Hãy chọn hoặc tìm một cái phù hợp với bạn (thuận tiện và dễ hiểu, hoặc linh hoạt nhất có thể).
Trong tham số Address
, mặc định là địa chỉ của endpoint
của API máy chủ httpbin.org
. "Trang web" động này trả về các tiêu đề HTTP của yêu cầu của nó (ở định dạng JSON) cho máy khách. Do đó, chúng ta sẽ có thể thấy trong chương trình của mình chính xác những gì đã đến máy chủ web từ terminal.
Đừng quên thêm miền "httpbin.org" vào danh sách được phép trong cài đặt terminal.
Định dạng văn bản JSON là tiêu chuẩn thực tế cho các dịch vụ web. Các triển khai sẵn có của các lớp để phân tích JSON có thể được tìm thấy trên trang mql5.com
, nhưng hiện tại, chúng ta sẽ chỉ hiển thị JSON "nguyên bản".
Trong trình xử lý OnStart
, chúng ta gọi WebRequest
với các tham số đã cho và xử lý kết quả nếu mã lỗi không âm. Các tiêu đề phản hồi của máy chủ (response
) luôn được ghi vào nhật ký.
void OnStart()
{
uchar data[], result[];
string response;
int code = PRTF(WebRequest(Method, Address, Headers, Timeout, data, result, response));
if(code > -1)
{
Print(response);
if(ArraySize(result) > 0)
{
PrintFormat("Got data: %d bytes", ArraySize(result));
if(DumpDataToFiles)
{
string parts[];
URL::parse(Address, parts);
const string filename = parts[URL_HOST] +
(StringLen(parts[URL_PATH]) > 1 ? parts[URL_PATH] : "/_index_.htm");
Print("Saving ", filename);
PRTF(FileSave(filename, result));
}
else
{
Print(CharArrayToString(result, 0, 80, CP_UTF8));
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Để tạo tên tệp, chúng ta sử dụng lớp trợ giúp URL từ tệp tiêu đề URL.mqh
(sẽ không được mô tả đầy đủ ở đây). Phương thức URL::parse
phân tích chuỗi được truyền thành các thành phần URL theo đặc tả vì dạng tổng quát của URL luôn là "protocol://domain.com:port/path?query#hash"; lưu ý rằng nhiều đoạn là tùy chọn. Kết quả được đặt trong mảng nhận, các chỉ số trong đó tương ứng với các phần cụ thể của URL và được mô tả trong liệt kê URL_PARTS:
enum URL_PARTS
{
URL_COMPLETE, // địa chỉ đầy đủ
URL_SCHEME, // giao thức
URL_USER, // tên người dùng/mật khẩu (đã lỗi thời, không được hỗ trợ)
URL_HOST, // máy chủ
URL_PORT, // số cổng
URL_PATH, // đường dẫn/thư mục
URL_QUERY, // chuỗi truy vấn sau '?'
URL_FRAGMENT, // đoạn sau '#' (không được đánh dấu)
URL_ENUM_LENGTH
};
2
3
4
5
6
7
8
9
10
11
12
Do đó, khi dữ liệu nhận được cần được ghi vào tệp, script tạo nó trong một thư mục được đặt tên theo máy chủ (parts[URL_HOST]
) và tiếp tục như vậy, giữ nguyên hệ thống phân cấp đường dẫn trong URL (parts[URL_PATH]
): trong trường hợp đơn giản nhất, đây chỉ đơn giản là tên của "endpoint". Khi trang chủ của một trang web được yêu cầu (đường dẫn chỉ chứa một dấu gạch chéo '/'), tệp được đặt tên là "index.htm".
Hãy thử chạy script với các tham số mặc định, nhớ cho phép máy chủ này trong cài đặt terminal trước. Trong nhật ký, chúng ta sẽ thấy các dòng sau (tiêu đề HTTP của phản hồi máy chủ và thông báo về việc lưu tệp thành công):
WebRequest(Method,Address,Headers,Timeout,data,result,response)=200 / ok
Date: Fri, 22 Jul 2022 08:45:03 GMT
Content-Type: application/json
Content-Length: 291
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Got data: 291 bytes
Saving httpbin.org/headers
FileSave(filename,result)=true / ok
2
3
4
5
6
7
8
9
10
11
12
Tệp httpbin.org/headers
chứa các tiêu đề của yêu cầu của chúng ta như được máy chủ nhìn thấy (máy chủ đã tự thêm định dạng JSON khi trả lời chúng ta).
{
"headers":
{
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "ru,en",
"Host": "httpbin.org",
"User-Agent": "MetaTrader 5 Terminal/5.3333 (Windows NT 10.0; Win64; x64)",
"X-Amzn-Trace-Id": "Root=1-62da638f-2554..." // <- điều này được thêm bởi máy chủ proxy ngược
}
}
2
3
4
5
6
7
8
9
10
11
Như vậy, terminal báo cáo rằng nó sẵn sàng chấp nhận dữ liệu của bất kỳ loại nào, với hỗ trợ nén bằng các phương pháp cụ thể và danh sách các ngôn ngữ ưu tiên. Ngoài ra, nó xuất hiện trong trường User-Agent dưới dạng MetaTrader 5. Điều này có thể không mong muốn khi làm việc với một số trang web được tối ưu hóa để chỉ hoạt động với trình duyệt. Khi đó, chúng ta có thể chỉ định một tên giả trong tham số đầu vào headers
, ví dụ, "User-Agent: Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36".
Một số trang thử nghiệm được liệt kê ở trên cho phép bạn tổ chức một môi trường thử nghiệm tạm thời trên máy chủ với một tên ngẫu nhiên cho thí nghiệm cá nhân của bạn: để làm điều này, bạn cần truy cập trang web từ trình duyệt và nhận một liên kết duy nhất thường hoạt động trong 24 giờ. Sau đó, bạn sẽ có thể sử dụng liên kết này làm địa chỉ cho các yêu cầu từ MQL5 và theo dõi hành vi của các yêu cầu trực tiếp từ trình duyệt. Tại đó, bạn cũng có thể cấu hình các phản hồi của máy chủ, đặc biệt là thử gửi biểu mẫu.
Hãy làm cho ví dụ này phức tạp hơn một chút. Máy chủ có thể yêu cầu các hành động bổ sung từ máy khách để thực hiện yêu cầu, đặc biệt là xác thực, thực hiện "chuyển hướng" (đi đến một địa chỉ khác), giảm tần suất yêu cầu, v.v. Tất cả các "tín hiệu" như vậy được biểu thị bằng các mã HTTP đặc biệt được hàm WebRequest
trả về. Ví dụ, mã 301 và 302 có nghĩa là chuyển hướng vì các lý do khác nhau, và WebRequest
thực hiện nó tự động bên trong, yêu cầu lại trang tại địa chỉ do máy chủ chỉ định (do đó, các mã chuyển hướng không bao giờ xuất hiện trong mã chương trình MQL). Mã 401 yêu cầu máy khách cung cấp tên người dùng và mật khẩu, và ở đây toàn bộ trách nhiệm thuộc về chúng ta. Có nhiều cách để gửi dữ liệu này. Một script mới WebRequestAuth.mq5
thể hiện việc xử lý hai tùy chọn xác thực mà máy chủ yêu cầu bằng tiêu đề phản hồi HTTP: "WWW-Authenticate: Basic" hoặc "WWW-Authenticate: Digest". Trong tiêu đề, nó có thể trông như thế này:
WWW-Authenticate:Basic realm="DemoBasicAuth"
Hoặc như thế này:
WWW-Authenticate:Digest realm="DemoDigestAuth",qop="auth",
» nonce="cuFAuHbb5UDvtFGkZEb2mNxjqEG/DjDr",opaque="fyNjGC4x8Zgt830PpzbXRvoqExsZeQSDZj"
2
Phương thức đầu tiên là đơn giản nhất và không an toàn nhất, vì vậy hầu như không được sử dụng: nó được đưa vào sách vì sự dễ học ở giai đoạn đầu. Ý chính của hoạt động của nó là tạo yêu cầu HTTP sau để đáp ứng yêu cầu của máy chủ bằng cách thêm một tiêu đề đặc biệt:
Authorization: Basic dXNlcjpwYXNzd29yZA==
Ở đây, từ khóa "Basic" được theo sau bởi chuỗi được mã hóa Base64 "user:password" với tên người dùng và mật khẩu thực tế, và ký tự ':' được chèn vào sau đó "nguyên bản" như một khối liên kết. Quá trình tương tác được thể hiện rõ ràng hơn trong hình ảnh.
Sơ đồ xác thực đơn giản trên máy chủ web
Sơ đồ xác thực Digest
được coi là tiên tiến hơn. Trong trường hợp này, máy chủ cung cấp một số thông tin bổ sung trong phản hồi của nó:
realms
— tên của trang web (khu vực trang web) nơi đăng nhập được thực hiệnqop
— một biến thể của phương thức Digest (chúng ta sẽ chỉ xem xét "auth")nonce
— một chuỗi ngẫu nhiên sẽ được sử dụng để tạo dữ liệu xác thựcopaque
— một chuỗi ngẫu nhiên mà chúng ta sẽ truyền lại "nguyên bản" trong tiêu đề của chúng taalgorithm
— tên tùy chọn của thuật toán băm, mặc định là MD5
Để xác thực, bạn cần thực hiện các bước sau:
- Tạo chuỗi ngẫu nhiên của riêng bạn
cnonce
- Khởi tạo hoặc tăng bộ đếm yêu cầu của bạn
nc
- Tính toán
hash1 = MD5(user:realm:password)
- Tính toán
hash2 = MD5(method:uri)
, ở đâyuri
là đường dẫn và tên của trang - Tính toán
response = MD5(hash1:nonce:nc:cnonce:qop:hash2)
Sau đó, máy khách có thể lặp lại yêu cầu đến máy chủ, thêm một dòng như thế này vào tiêu đề của nó:
Authorization: Digest username="user",realm="realm",nonce="...",
» uri="/path/to/page",qop=auth,nc=00000001,cnonce="...",response="...",opaque="..."
2
Vì máy chủ có cùng thông tin như máy khách, nó sẽ có thể lặp lại các phép tính và kiểm tra xem các hàm băm có khớp không.
Hãy thêm các biến vào tham số script để nhập tên người dùng và mật khẩu. Mặc định, tham số Address
bao gồm địa chỉ của điểm cuối digest-auth
, có thể yêu cầu xác thực với các tham số qop
("auth"), đăng nhập ("test"), và mật khẩu ("pass"). Tất cả điều này là tùy chọn trong đường dẫn điểm cuối (bạn có thể thử các phương thức và thông tin đăng nhập người dùng khác, như vậy: "https://httpbin.org/digest-auth/auth-int/mql5client/mql5password").
const string Method = "GET";
input string Address = "https://httpbin.org/digest-auth/auth/test/pass";
input string Headers = "User-Agent: noname";
input int Timeout = 5000;
input string User = "test";
input string Password = "pass";
input bool DumpDataToFiles = true;
2
3
4
5
6
7
Chúng ta đã chỉ định một tên trình duyệt giả trong tham số Headers
để thể hiện tính năng này.
Trong hàm OnStart
, chúng ta thêm xử lý mã HTTP 401. Nếu tên người dùng và mật khẩu không được cung cấp, chúng ta sẽ không thể tiếp tục.
void OnStart()
{
string parts[];
URL::parse(Address, parts);
uchar data[], result[];
string response;
int code = PRTF(WebRequest(Method, Address, Headers, Timeout, data, result, response));
Print(response);
if(code == 401)
{
if(StringLen(User) == 0 || StringLen(Password) == 0)
{
Print("Credentials required");
return;
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Bước tiếp theo là phân tích các tiêu đề nhận được từ máy chủ. Để tiện lợi, chúng ta đã viết lớp HttpHeader
(HttpHeader.mqh
). Văn bản đầy đủ được truyền vào hàm khởi tạo của nó, cũng như ký tự phân tách phần tử (trong trường hợp này là ký tự xuống dòng '\n') và ký tự được sử dụng giữa tên và giá trị trong mỗi phần tử (trong trường hợp này là dấu hai chấm '😂. Trong quá trình tạo, đối tượng "phân tích" văn bản, sau đó các phần tử được cung cấp thông qua toán tử [] được nạp chồng, với kiểu của đối số là chuỗi. Kết quả là, chúng ta có thể kiểm tra yêu cầu xác thực bằng tên "WWW-Authenticate". Nếu phần tử như vậy tồn tại trong văn bản và bằng "Basic", chúng ta tạo tiêu đề phản hồi "Authorization: Basic" với thông tin đăng nhập và mật khẩu được mã hóa trong Base64.
code = -1;
HttpHeader header(response, '\n', ':');
const string auth = header["WWW-Authenticate"];
if(StringFind(auth, "Basic ") == 0)
{
string Header = Headers;
if(StringLen(Header) > 0) Header += "\r\n";
Header += "Authorization: Basic ";
Header += HttpHeader::hash(User + ":" + Password, CRYPT_BASE64);
PRTF(Header);
code = PRTF(WebRequest(Method, Address, Header, Timeout, data, result, response));
Print(response);
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
Đối với xác thực Digest, mọi thứ phức tạp hơn một chút, tuân theo thuật toán đã nêu ở trên.
else if(StringFind(auth, "Digest ") == 0)
{
HttpHeader params(StringSubstr(auth, 7), ',', '=');
string realm = HttpHeader::unquote(params["realm"]);
if(realm != NULL)
{
string qop = HttpHeader::unquote(params["qop"]);
if(qop == "auth")
{
string h1 = HttpHeader::hash(User + ":" + realm + ":" + Password);
string h2 = HttpHeader::hash(Method + ":" + parts[URL_PATH]);
string nonce = HttpHeader::unquote(params["nonce"]);
string counter = StringFormat("%08x", 1);
string cnonce = StringFormat("%08x", MathRand());
string h3 = HttpHeader::hash(h1 + ":" + nonce + ":" + counter + ":" +
cnonce + ":" + qop + ":" + h2);
string Header = Headers;
if(StringLen(Header) > 0) Header += "\r\n";
Header += "Authorization: Digest ";
Header += "username=\"" + User + "\",";
Header += "realm=\"" + realm + "\",";
Header += "nonce=\"" + nonce + "\",";
Header += "uri=\"" + parts[URL_PATH] + "\",";
Header += "qop=" + qop + ",";
Header += "nc=" + counter + ",";
Header += "cnonce=\"" + cnonce + "\",";
Header += "response=\"" + h3 + "\",";
Header += "opaque=" + params["opaque"] + "";
PRTF(Header);
code = PRTF(WebRequest(Method, Address, Header, Timeout, data, result, response));
Print(response);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Phương thức tĩnh HttpHeader::hash
nhận một chuỗi với biểu diễn băm thập lục phân (mặc định là MD5) cho tất cả các chuỗi hợp thành cần thiết. Dựa trên dữ liệu này, tiêu đề được tạo cho lần gọi WebRequest
tiếp theo. Phương thức tĩnh HttpHeader::unquote
loại bỏ các dấu ngoặc kép bao quanh.
Phần còn lại của script không thay đổi. Một yêu cầu HTTP lặp lại có thể thành công, và sau đó chúng ta sẽ nhận được nội dung của trang bảo mật, hoặc xác thực sẽ bị từ chối, và máy chủ sẽ ghi gì đó như "Truy cập bị từ chối".
Vì các tham số mặc định chứa các giá trị chính xác ("/digest-auth/auth/test/pass" tương ứng với người dùng "test" và mật khẩu "pass"), chúng ta sẽ nhận được kết quả sau khi chạy script (tất cả các bước chính và dữ liệu được ghi vào nhật ký).
WebRequest(Method,Address,Headers,Timeout,data,result,response)=401 / ok
Date: Fri, 22 Jul 2022 10:45:56 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 0
Connection: keep-alive
Server: gunicorn/19.9.0
WWW-Authenticate: Digest realm="[email protected]" »
» nonce="87d28b529a7a8797f6c3b81845400370", qop="auth",
» opaque="4cb97ad7ea915a6d24cf1ccbf6feeaba", algorithm=MD5, stale=FALSE
...
2
3
4
5
6
7
8
9
10
Lần gọi WebRequest
đầu tiên kết thúc với mã 401, và trong số các tiêu đề phản hồi có yêu cầu xác thực ("WWW-Authenticate") với các tham số cần thiết. Dựa trên chúng, chúng ta đã tính toán câu trả lời chính xác và chuẩn bị tiêu đề cho yêu cầu mới.
Header=User-Agent: noname
Authorization: Digest username="test",realm="[email protected]" »
» nonce="87d28b529a7a8797f6c3b81845400370",uri="/digest-auth/auth/test/pass",
» qop=auth,nc=00000001,cnonce="00001c74",
» response="c09e52bca9cc90caf9a707d046b567b2",opaque="4cb97ad7ea915a6d24cf1ccbf6feeaba" / ok
...
2
3
4
5
6
Yêu cầu thứ hai trả về 200 và dữ liệu tải xuống mà chúng ta ghi vào tệp.
WebRequest(Method,Address,Header,Timeout,data,result,response)=200 / ok
Date: Fri, 22 Jul 2022 10:45:56 GMT
Content-Type: application/json
Content-Length: 47
Connection: keep-alive
Server: gunicorn/19.9.0
...
Got data: 47 bytes
Saving httpbin.org/digest-auth/auth/test/pass
FileSave(filename,result)=true / ok
2
3
4
5
6
7
8
9
10
Bên trong tệp MQL5/Files/httpbin.org/digest-auth/auth/test/pass
, bạn có thể tìm thấy "trang web", hoặc đúng hơn là trạng thái xác thực thành công ở định dạng JSON.
{
"authenticated": true,
"user": "test"
}
2
3
4
Nếu bạn chỉ định sai mật khẩu khi chạy script, chúng ta sẽ nhận được phản hồi trống từ máy chủ, và tệp sẽ không được ghi.
Sử dụng
WebRequest
, chúng ta tự động bước vào lĩnh vực các hệ thống phần mềm phân tán, trong đó hoạt động đúng không chỉ phụ thuộc vào mã MQL phía máy khách của chúng ta mà còn vào máy chủ (chưa kể đến các liên kết trung gian, như proxy). Do đó, bạn cần chuẩn bị cho việc xảy ra lỗi của người khác. Cụ thể, tại thời điểm viết sách, trong việc triển khai điểm cuốidigest-auth
trênhttpbin.org
đã có vấn đề: tên người dùng nhập vào yêu cầu không tham gia vào quá trình kiểm tra ủy quyền, và do đó bất kỳ thông tin đăng nhập nào cũng dẫn đến ủy quyền thành công nếu mật khẩu đúng được chỉ định. Dù vậy, để kiểm tra script của chúng ta, hãy sử dụng các dịch vụ khác, ví dụ, nhưhttpbingo.org/digest-auth/auth/test/pass
. Bạn cũng có thể cấu hình script đến địa chỉjigsaw.w3.org/HTTP/Digest/
— nó mong đợi thông tin đăng nhập/mật khẩu làguest
/guest
.
Trong thực tế, hầu hết các trang web triển khai ủy quyền bằng cách sử dụng các biểu mẫu được nhúng trực tiếp vào các trang web: bên trong mã HTML, chúng thực chất là thẻ chứa form
với một tập hợp các trường nhập liệu, được người dùng điền vào và gửi đến máy chủ bằng phương thức POST. Về vấn đề này, việc phân tích ví dụ về việc gửi biểu mẫu là hợp lý. Tuy nhiên, trước khi đi sâu vào chi tiết này, nên làm nổi bật một kỹ thuật khác.
Vấn đề là sự tương tác giữa máy khách và máy chủ thường đi kèm với sự thay đổi trạng thái của cả hai phía. Lấy ví dụ về ủy quyền, điều này có thể được hiểu rõ nhất, vì trước khi ủy quyền, người dùng chưa được hệ thống biết đến, và sau đó, hệ thống đã biết thông tin đăng nhập và có thể áp dụng các cài đặt ưu tiên cho trang web (ví dụ, ngôn ngữ, màu sắc, cách hiển thị diễn đàn), đồng thời cho phép truy cập vào những trang mà khách truy cập chưa được ủy quyền không thể vào (máy chủ ngăn chặn các nỗ lực như vậy bằng cách trả về trạng thái HTTP 403, Cấm).
Hỗ trợ và đồng bộ hóa trạng thái nhất quán của phần máy khách và máy chủ trong ứng dụng web phân tán được cung cấp bằng cơ chế cookie, ngụ ý các biến có tên và giá trị của chúng trong tiêu đề HTTP. Thuật ngữ này bắt nguồn từ "fortune cookies" vì cookies
cũng chứa các thông điệp nhỏ không hiển thị cho người dùng.
Cả hai phía, máy chủ và máy khách, đều có thể thêm cookie
vào tiêu đề HTTP. Máy chủ thực hiện điều này bằng một dòng như:
Set-Cookie: name=value; Domain=domain; Path=path; Expires=date; Max-Age=number_of_seconds ...
Chỉ có tên và giá trị là bắt buộc, còn các thuộc tính còn lại là tùy chọn: đây là những thuộc tính chính − Domain
, Path
, Expires
, và Max age
, nhưng trong các tình huống thực tế, còn có nhiều thuộc tính hơn.
Sau khi nhận được tiêu đề như vậy (hoặc nhiều tiêu đề), máy khách phải ghi nhớ tên và giá trị của biến và gửi chúng đến máy chủ trong tất cả các yêu cầu nhắm đến Domain
và Path
tương ứng trong miền này cho đến ngày hết hạn (Expires
hoặc Max-Age
).
Trong yêu cầu HTTP gửi đi từ máy khách, cookies
được truyền dưới dạng chuỗi:
Cookie: name⁽№⁾=value⁽№⁾ ; name⁽ⁱ⁾=value⁽ⁱ⁾ ...
Ở đây, được phân tách bằng dấu chấm phẩy và khoảng trắng, tất cả các cặp name=value được liệt kê; chúng được máy chủ thiết lập và máy khách này biết đến, khớp với yêu cầu hiện tại theo miền và đường dẫn, và chưa hết hạn.
Máy chủ và máy khách trao đổi tất cả các cookie cần thiết với mỗi yêu cầu HTTP, đó là lý do tại sao phong cách kiến trúc này của các hệ thống phân tán được gọi là REST (Representational State Transfer
). Ví dụ, sau khi người dùng đăng nhập thành công vào máy chủ, máy chủ đặt (thông qua tiêu đề Set-Cookie:
) một cookie
đặc biệt với định danh của người dùng, sau đó trình duyệt web (hoặc trong trường hợp của chúng ta, một terminal với chương trình MQL) sẽ gửi nó trong các yêu cầu tiếp theo (bằng cách thêm dòng tương ứng vào tiêu đề Cookie:
).
Hàm WebRequest
âm thầm thực hiện tất cả công việc này cho chúng ta: thu thập cookie từ các tiêu đề đến và thêm các cookie thích hợp vào các yêu cầu HTTP gửi đi.
Cookie được terminal lưu trữ và giữa các phiên, theo cài đặt của chúng. Để kiểm tra điều này, chỉ cần yêu cầu một trang web hai lần từ một trang sử dụng cookie.
Chú ý, cookie được lưu trữ liên quan đến trang web và do đó được thay thế một cách không rõ ràng trong các tiêu đề gửi đi của tất cả các chương trình MQL sử dụng
WebRequest
cho cùng một trang web.
Để đơn giản hóa các yêu cầu liên tiếp, việc chính thức hóa các hành động phổ biến trong một lớp đặc biệt HTTPRequest
(HTTPRequest.mqh
) là hợp lý. Chúng ta sẽ lưu trữ các tiêu đề HTTP chung trong đó, có khả năng cần thiết cho tất cả các yêu cầu (ví dụ, ngôn ngữ được hỗ trợ, hướng dẫn cho proxy, v.v.). Ngoài ra, cài đặt như thời gian chờ cũng là chung. Cả hai cài đặt này được truyền vào hàm khởi tạo của đối tượng.
class HTTPRequest: public HttpCookie
{
protected:
string common_headers;
int timeout;
public:
HTTPRequest(const string h, const int t = 5000):
common_headers(h), timeout(t) { }
...
2
3
4
5
6
7
8
9
10
Theo mặc định, thời gian chờ được đặt là 5 giây. Phương thức chính, theo một nghĩa nào đó là phương thức phổ quát của lớp, là request
.
int request(const string method, const string address,
string headers, const uchar &data[], uchar &result[], string &response)
{
if(headers == NULL) headers = common_headers;
ArrayResize(result, 0);
response = NULL;
Print(">> Request:\n", method + " " + address + "\n" + headers);
const int code = PRTF(WebRequest(method, address, headers, timeout, data, result, response));
Print("<<< Response:\n", response);
return code;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
Hãy mô tả thêm một vài phương thức cho các truy vấn của các loại cụ thể.
Yêu cầu GET chỉ sử dụng tiêu đề và phần thân của tài liệu (thuật ngữ payload
thường được sử dụng) là trống.
int GET(const string address, uchar &result[], string &response,
const string custom_headers = NULL)
{
uchar nodata[];
return request("GET", address, custom_headers, nodata, result, response);
}
2
3
4
5
6
Trong yêu cầu POST, thường có một payload
.
int POST(const string address, const uchar &payload[],
uchar &result[], string &response, const string custom_headers = NULL)
{
return request("POST", address, custom_headers, payload, result, response);
}
2
3
4
5
Các biểu mẫu có thể được gửi ở các định dạng khác nhau. Định dạng đơn giản nhất là application/x-www-form-urlencoded
. Nó ngụ ý rằng payload
sẽ là một chuỗi (có thể rất dài, vì thông số kỹ thuật không áp đặt giới hạn, và tất cả phụ thuộc vào cài đặt của máy chủ web). Đối với các biểu mẫu như vậy, chúng ta sẽ cung cấp một phiên bản quá tải tiện lợi hơn của phương thức POST với tham số chuỗi payload
.
int POST(const string address, const string payload,
uchar &result[], string &response, const string custom_headers = NULL)
{
uchar bytes[];
const int n = StringToCharArray(payload, bytes, 0, -1, CP_UTF8);
ArrayResize(bytes, n - 1); // remove terminal zero
return request("POST", address, custom_headers, bytes, result, response);
}
2
3
4
5
6
7
8
Hãy viết một script đơn giản để kiểm tra công cụ web phía máy khách của chúng ta WebRequestCookie.mq5
. Nhiệm vụ của nó sẽ là yêu cầu cùng một trang web hai lần: lần đầu tiên máy chủ có thể sẽ đề nghị đặt cookie của nó, và sau đó chúng sẽ được thay thế tự động trong yêu cầu thứ hai. Trong các tham số đầu vào, chỉ định địa chỉ của trang để kiểm tra: hãy để đó là trang web mql5.com
. Chúng ta cũng sẽ mô phỏng các tiêu đề mặc định bằng chuỗi User-Agent
đã được sửa đổi.
input string Address = "https://www.mql5.com";
input string Headers = "User-Agent: Mozilla/5.0 (Windows NT 10.0) Chrome/103.0.0.0"; // Headers (use '|' as separator, if many specified)
2
Trong hàm chính của script, chúng ta mô tả đối tượng HTTPRequest
và thực hiện hai yêu cầu GET trong một vòng lặp.
Chú ý! Bài kiểm tra này hoạt động dựa trên giả định rằng các chương trình MQL chưa từng truy cập trang web www.mql5.com và chưa nhận được cookie từ nó. Sau khi chạy script một lần, cookie sẽ vẫn còn trong bộ nhớ cache của terminal, và sẽ không thể tái hiện ví dụ: trên cả hai lần lặp của vòng lặp, chúng ta sẽ nhận được cùng các mục nhật ký.
Đừng quên thêm miền
www.mql5.com
vào danh sách được phép trong cài đặt terminal.
void OnStart()
{
uchar result[];
string response;
HTTPRequest http(Headers);
for(int i = 0; i < 2; ++i)
{
if(http.GET(Address, result, response) > -1)
{
if(ArraySize(result) > 0)
{
PrintFormat("Got data: %d bytes", ArraySize(result));
if(i == 0) // show the beginning of the document only the first time
{
const string s = CharArrayToString(result, 0, 160, CP_UTF8);
int j = -1, k = -1;
while((j = StringFind(s, "\r\n", j + 1)) != -1) k = j;
Print(StringSubstr(s, 0, k));
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Lần lặp đầu tiên của vòng lặp sẽ tạo ra các mục nhật ký sau (có rút gọn):
>>> Request:
GET https://www.mql5.com
User-Agent: Mozilla/5.0 (Windows NT 10.0) Chrome/103.0.0.0
WebRequest(method,address,headers,timeout,data,result,response)=200 / ok
<<< Response:
Server: nginx
Date: Sun, 24 Jul 2022 19:04:35 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Cache-Control: no-cache,no-store
Content-Encoding: gzip
Expires: -1
Pragma: no-cache
Set-Cookie: sid=CfDJ8O2AwC...Ne2yP5QXpPKA2; domain=.mql5.com; path=/; samesite=lax; httponly
Vary: Accept-Encoding
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self' ...
Generate-Time: 2823
Agent-Type: desktop-ru-en
X-Cache-Status: MISS
Got data: 184396 bytes
<!DOCTYPE html>
<html lang="ru">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Chúng ta đã nhận được một cookie mới với tên sid
. Để xác minh hiệu quả của nó, bạn chuyển sang xem phần thứ hai của nhật ký, cho lần lặp thứ hai của vòng lặp.
>>> Request:
GET https://www.mql5.com
User-Agent: Mozilla/5.0 (Windows NT 10.0) Chrome/103.0.0.0
WebRequest(method,address,headers,timeout,data,result,response)=200 / ok
<<< Response:
Server: nginx
Date: Sun, 24 Jul 2022 19:04:36 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Cache-Control: no-cache, no-store, must-revalidate, no-transform
Content-Encoding: gzip
Expires: -1
Pragma: no-cache
Vary: Accept-Encoding
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self' ...
Generate-Time: 2950
Agent-Type: desktop-ru-en
X-Cache-Status: MISS
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Thật không may, ở đây chúng ta không thấy toàn bộ các tiêu đề gửi đi được hình thành bên trong WebRequest
, nhưng việc cookie được gửi đến máy chủ bằng tiêu đề Cookie:
được chứng minh bởi thực tế là máy chủ trong phản hồi thứ hai của nó không còn yêu cầu đặt lại cookie đó.
Về lý thuyết, cookie này chỉ đơn giản là xác định khách truy cập (như hầu hết các trang web làm) nhưng không biểu thị sự ủy quyền của họ. Do đó, hãy quay lại bài tập gửi biểu mẫu theo cách chung, nghĩa là trong tương lai là nhiệm vụ riêng tư về việc nhập thông tin đăng nhập và mật khẩu.
Nhớ lại rằng để gửi biểu mẫu, chúng ta có thể sử dụng phương thức POST với tham số chuỗi payload
. Nguyên tắc chuẩn bị dữ liệu theo tiêu chuẩn x-www-form-urlencoded
là các biến có tên và giá trị của chúng được ghi trong một dòng liên tục (hơi giống cookie).
name⁽№⁾=value⁽№⁾[&name⁽ⁱ⁾=value⁽ⁱ⁾...]ᵒᵖᵗ
Tên và giá trị được nối với dấu '=', và các cặp được nối với nhau bằng ký tự '&'. Giá trị có thể bị thiếu. Ví dụ,
Name=John&Age=33&Education=&Address=
Điều quan trọng cần lưu ý là từ góc độ kỹ thuật, chuỗi này phải được chuyển đổi theo thuật toán trước khi gửi urlencode
(đây là nơi xuất phát tên của định dạng), tuy nhiên, WebRequest
thực hiện sự chuyển đổi này cho chúng ta.
Tên biến được xác định bởi biểu mẫu web (nội dung của thẻ form
trong một trang web) hoặc logic ứng dụng web - trong bất kỳ trường hợp nào, máy chủ web phải có khả năng diễn giải các tên và giá trị. Do đó, để làm quen với công nghệ, chúng ta cần một máy chủ thử nghiệm với một biểu mẫu.
Biểu mẫu thử nghiệm có sẵn tại https://httpbin.org/forms/post
. Đó là một hộp thoại để đặt hàng pizza.
Biểu mẫu web thử nghiệm
Cấu trúc bên trong và hành vi của nó được mô tả bởi mã HTML sau. Trong đó, chúng ta chủ yếu quan tâm đến các thẻ input
, thiết lập các biến mà máy chủ mong đợi. Ngoài ra, cần chú ý đến thuộc tính action
trong thẻ form
, vì nó xác định địa chỉ mà yêu cầu POST nên được gửi đến, và trong trường hợp này là /post
, khi kết hợp với miền sẽ cho chuỗi httpbin.org/post
. Đây là những gì chúng ta sẽ sử dụng trong chương trình MQL.
<!DOCTYPE html>
<html>
<body>
<form method="post" action="/post">
<p><label>Customer name: <input name="custname"></label></p>
<p><label>Telephone: <input type=tel name="custtel"></label></p>
<p><label>E-mail address: <input type=email name="custemail"></label></p>
<fieldset>
<legend> Pizza Size </legend>
<p><label> <input type=radio name=size value="small"> Small </label></p>
<p><label> <input type=radio name=size value="medium"> Medium </label></p>
<p><label> <input type=radio name=size value="large"> Large </label></p>
</fieldset>
<fieldset>
<legend> Pizza Toppings </legend>
<p><label> <input type=checkbox name="topping" value="bacon"> Bacon </label></p>
<p><label> <input type=checkbox name="topping" value="cheese"> Extra Cheese </label></p>
<p><label> <input type=checkbox name="topping" value="onion"> Onion </label></p>
<p><label> <input type=checkbox name="topping" value="mushroom"> Mushroom </label></p>
</fieldset>
<p><label>Preferred delivery time: <input type=time min="11:00" max="21:00" step="900" name="delivery"></label></p>
<p><label>Delivery instructions: <textarea name="comments"></textarea></label></p>
<p><button>Submit order</button></p>
</form>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Trong script WebRequestForm.mq5
, chúng ta đã chuẩn bị các biến đầu vào tương tự để người dùng chỉ định trước khi gửi đến máy chủ.
input string Address = "https://httpbin.org/post";
input string Customer = "custname=Vincent Silver";
input string Telephone = "custtel=123-123-123";
input string Email = "[email protected]";
input string PizzaSize = "size=small"; // PizzaSize (small,medium,large)
input string PizzaTopping = "topping=bacon"; // PizzaTopping (bacon,cheese,onion,mushroom)
input string DeliveryTime = "delivery=";
input string Comments = "comments=";
2
3
4
5
6
7
8
9
Các chuỗi đã đặt chỉ được hiển thị để kiểm tra bằng một cú nhấp chuột: bạn có thể thay thế chúng bằng chuỗi của riêng mình, nhưng lưu ý rằng trong mỗi chuỗi chỉ nên chỉnh sửa giá trị bên phải của '=', và tên bên trái của '=' nên được giữ nguyên (các tên không biết sẽ bị máy chủ bỏ qua).
Trong hàm OnStart
, chúng ta mô tả tiêu đề HTTP Content-Type:
và chuẩn bị một chuỗi nối với tất cả các biến.
void OnStart()
{
uchar result[];
string response;
string header = "Content-Type: application/x-www-form-urlencoded";
string form_fields;
StringConcatenate(form_fields,
Customer, "&",
Telephone, "&",
Email, "&",
PizzaSize, "&",
PizzaTopping, "&",
DeliveryTime, "&",
Comments);
HTTPRequest http;
if(http.POST(Address, form_fields, result, response) > -1)
{
if(ArraySize(result) > 0)
{
PrintFormat("Got data: %d bytes", ArraySize(result));
// NB: UTF-8 is implied for many content-types,
// but some may be different, analyze the response headers
Print(CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8));
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Sau đó, chúng ta thực hiện phương thức POST và ghi lại phản hồi của máy chủ. Dưới đây là một ví dụ kết quả.
>>> Request:
POST https://httpbin.org/post
Content-Type: application/x-www-form-urlencoded
WebRequest(method,address,headers,timeout,data,result,response)=200 / ok
<<< Response:
Date: Mon, 25 Jul 2022 08:41:41 GMT
Content-Type: application/json
Content-Length: 780
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Got data: 721 bytes
{
"args": {},
"data": "",
"files": {},
"form": {
"comments": "",
"custemail": "[email protected]",
"custname": "Vincent Silver",
"custtel": "123-123-123",
"delivery": "",
"size": "small",
"topping": "bacon"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "ru,en",
"Content-Length": "127",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "MetaTrader 5 Terminal/5.3333 (Windows NT 10.0; x64)",
"X-Amzn-Trace-Id": "Root=1-62de5745-25bd1d823a9609f01cff04ad"
},
"json": null,
"url": "https://httpbin.org/post"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Máy chủ thử nghiệm xác nhận nhận được dữ liệu dưới dạng một bản sao JSON. Trong thực tế, máy chủ, tất nhiên, sẽ không trả lại chính dữ liệu, mà chỉ báo cáo trạng thái thành công và có thể chuyển hướng đến một trang web khác mà dữ liệu đã ảnh hưởng đến (ví dụ, hiển thị số đơn hàng).
Với sự trợ giúp của các yêu cầu POST như vậy, nhưng nhỏ hơn về kích thước, việc ủy quyền thường cũng được thực hiện. Nhưng nói thật, hầu hết các dịch vụ web cố tình làm phức tạp quá trình này vì mục đích bảo mật và yêu cầu bạn tính toán trước một số tổng băm từ chi tiết của người dùng. Các API công khai được phát triển đặc biệt thường có mô tả tất cả các thuật toán cần thiết trong tài liệu. Nhưng không phải lúc nào cũng vậy. Đặc biệt, chúng ta sẽ không thể đăng nhập bằng WebRequest
trên mql5.com
vì trang web không có giao diện lập trình mở.
Khi gửi yêu cầu đến các dịch vụ web, luôn tuân thủ quy tắc không vượt quá tần suất yêu cầu: thường thì mỗi dịch vụ chỉ định giới hạn riêng của nó, và việc vi phạm chúng sẽ dẫn đến việc chặn chương trình máy khách, tài khoản hoặc địa chỉ IP của bạn sau đó.