Đọc và ghi dữ liệu qua kết nối socket không bảo mật
Theo lịch sử, socket cung cấp truyền dữ liệu qua kết nối đơn giản theo mặc định. Việc truyền dữ liệu ở dạng mở cho phép các phương tiện kỹ thuật phân tích toàn bộ lưu lượng. Trong những năm gần đây, các vấn đề bảo mật được xem xét nghiêm túc hơn và do đó công nghệ TLS (Bảo mật Tầng Giao vận) đã được triển khai hầu như khắp mọi nơi: nó cung cấp mã hóa tức thì cho tất cả dữ liệu giữa người gửi và người nhận. Đặc biệt, đối với kết nối Internet, sự khác biệt nằm ở giao thức HTTP (kết nối đơn giản) và HTTPS (bảo mật).
MQL5 cung cấp các tập hợp hàm Socket
khác nhau để làm việc với kết nối đơn giản và bảo mật. Trong phần này, chúng ta sẽ làm quen với chế độ đơn giản, và sau đó sẽ chuyển sang chế độ được bảo vệ.
Để đọc dữ liệu từ socket, sử dụng hàm SocketRead
.
int SocketRead(int socket, uchar &buffer[], uint maxlen, uint timeout)
Mô tả socket được lấy từ SocketCreate
và được kết nối với tài nguyên mạng bằng SocketConnect
.
Tham số buffer
là tham chiếu đến mảng mà dữ liệu sẽ được đọc vào. Nếu mảng là động, kích thước của nó tăng lên theo số byte đã đọc, nhưng không thể vượt quá INT_MAX (2147483647). Bạn có thể giới hạn số byte đọc trong tham số maxlen
. Dữ liệu không vừa sẽ vẫn còn trong bộ đệm nội bộ của socket: nó có thể được lấy bằng lệnh gọi SocketRead
tiếp theo. Giá trị của maxlen
phải nằm trong khoảng từ 1 đến INT_MAX (2147483647).
Tham số timeout
chỉ định thời gian (tính bằng mili giây) để đợi việc đọc hoàn tất. Nếu không có dữ liệu nào được nhận trong thời gian này, các nỗ lực sẽ bị chấm dứt và hàm thoát với kết quả -1.
-1 cũng được trả về khi xảy ra lỗi, trong khi mã lỗi trong _LastError
, ví dụ, 5273 (ERR_NETSOCKET_IO_ERROR), có nghĩa là kết nối được thiết lập qua SocketConnect
hiện đã bị ngắt.
Nếu thành công, hàm trả về số byte đã đọc.
Khi đặt thời gian chờ đọc là 0, giá trị mặc định 120000 (2 phút) được sử dụng.
Để ghi dữ liệu vào socket, sử dụng hàm SocketSend
.
Thật không may, tên hàm
SocketRead
vàSocketSend
không "đối xứng": thao tác ngược lại với "read" là "write", và với "send" là "receive". Điều này có thể không quen thuộc với các nhà phát triển có kinh nghiệm làm việc với API mạng trên các nền tảng khác.
int SocketSend(int socket, const uchar &buffer[], uint maxlen)
Tham số đầu tiên là tay cầm của một socket đã được tạo và mở trước đó. Khi truyền một tay cầm không hợp lệ, _LastError
nhận lỗi 5270 (ERR_NETSOCKET_INVALIDHANDLE). Mảng buffer
chứa dữ liệu cần gửi với kích thước dữ liệu được chỉ định trong tham số maxlen
(tham số này được giới thiệu để thuận tiện cho việc gửi một phần dữ liệu từ mảng cố định).
Hàm trả về số byte đã ghi vào socket khi thành công và -1 khi lỗi.
Các lỗi cấp hệ thống (5273, ERR_NETSOCKET_IO_ERROR) cho thấy kết nối bị ngắt.
Script SocketReadWriteHTTP.mq5
thể hiện cách socket có thể được sử dụng để triển khai công việc qua giao thức HTTP, tức là yêu cầu thông tin về một trang từ máy chủ web. Đây là một phần nhỏ của những gì hàm WebRequest
thực hiện cho chúng ta "phía sau hậu trường".
Hãy để địa chỉ mặc định trong các tham số đầu vào: trang web "www.mql5.com". Số cổng được chọn là 80 vì đó là giá trị mặc định cho các kết nối HTTP không bảo mật (mặc dù một số máy chủ có thể sử dụng cổng khác: 81, 8080, v.v.). Các cổng dành riêng cho kết nối bảo mật (đặc biệt là cổng phổ biến nhất 443) chưa được hỗ trợ bởi ví dụ này. Ngoài ra, trong tham số Server
, điều quan trọng là nhập tên miền chứ không phải một trang cụ thể vì script chỉ có thể yêu cầu trang chính, tức là đường dẫn gốc "/".
input string Server = "www.mql5.com";
input uint Port = 80;
2
Trong hàm chính của script, chúng ta sẽ tạo một socket và mở kết nối trên đó với các tham số được chỉ định (thời gian chờ là 5 giây).
void OnStart()
{
PRTF(Server);
PRTF(Port);
const int socket = PRTF(SocketCreate());
if(PRTF(SocketConnect(socket, Server, Port, 5000)))
{
...
}
}
2
3
4
5
6
7
8
9
10
Hãy xem cách giao thức HTTP hoạt động. Máy khách gửi các yêu cầu dưới dạng tiêu đề được thiết kế đặc biệt (chuỗi với tên và giá trị được định nghĩa trước), bao gồm, đặc biệt, địa chỉ trang web, và máy chủ gửi toàn bộ trang web hoặc trạng thái hoạt động để phản hồi, cũng sử dụng các tiêu đề đặc biệt cho việc này. Máy khách có thể yêu cầu một trang web bằng yêu cầu GET, gửi một số dữ liệu bằng yêu cầu POST, hoặc kiểm tra trạng thái của trang web bằng yêu cầu HEAD tiết kiệm. Về lý thuyết, còn có nhiều phương thức HTTP khác — bạn có thể tìm hiểu về chúng trong thông số kỹ thuật giao thức HTTP.
Do đó, script phải tạo và gửi một tiêu đề HTTP qua kết nối socket. Ở dạng đơn giản nhất, yêu cầu HEAD sau đây cho phép lấy thông tin meta về trang (chúng ta có thể thay HEAD bằng GET để yêu cầu toàn bộ trang nhưng có một số phức tạp; chúng ta sẽ thảo luận điều này sau).
HEAD / HTTP/1.1
Host: _server_
User-Agent: MetaTrader 5
// <- hai dòng mới liên tiếp \r\n\r\n
2
3
4
Dấu gạch chéo sau "HEAD" (hoặc phương thức khác) là đường dẫn ngắn nhất có thể trên bất kỳ máy chủ nào đến thư mục gốc, thường dẫn đến việc hiển thị trang chính. Nếu chúng ta muốn một trang web cụ thể, chúng ta có thể viết gì đó như "GET /en/forum/ HTTP/1.1" và nhận bảng nội dung của các diễn đàn tiếng Anh từ mql5.com
. Chỉ định một tên miền thực thay vì chuỗi "server".
Mặc dù sự hiện diện của "User-Agent:" là tùy chọn, nó cho phép chương trình "giới thiệu bản thân" với máy chủ, nếu không có nó, một số máy chủ có thể từ chối yêu cầu.
Lưu ý hai dòng trống: chúng đánh dấu phần kết thúc của tiêu đề. Trong script của chúng ta, việc tạo tiêu đề bằng biểu thức sau là tiện lợi:
StringFormat("HEAD / HTTP/1.1\r\nHost: %s\r\n\r\n", Server)
Bây giờ chúng ta chỉ cần gửi nó đến máy chủ. Cho mục đích này, chúng ta đã viết một hàm đơn giản HTTPSend
. Nó nhận một mô tả socket và một dòng tiêu đề.
bool HTTPSend(int socket, const string request)
{
char req[];
int len = StringToCharArray(request, req, 0, WHOLE_ARRAY, CP_UTF8) - 1;
if(len < 0) return false;
return SocketSend(socket, req, len) == len;
}
2
3
4
5
6
7
Bên trong, chúng ta chuyển đổi chuỗi thành mảng byte và gọi SocketSend
.
Tiếp theo, chúng ta cần chấp nhận phản hồi từ máy chủ, cho việc này chúng ta đã viết hàm HTTPRecv
. Nó cũng mong đợi một mô tả socket và một tham chiếu đến chuỗi nơi dữ liệu nên được đặt nhưng phức tạp hơn.
bool HTTPRecv(int socket, string &result, const uint timeout)
{
char response[];
int len; // số nguyên có dấu cần thiết cho cờ lỗi -1
uint start = GetTickCount();
result = "";
do
{
ResetLastError();
if(!(len = (int)SocketIsReadable(socket)))
{
Sleep(10); // đợi dữ liệu hoặc hết thời gian
}
else // đọc dữ liệu trong khối lượng có sẵn
if((len = SocketRead(socket, response, len, timeout)) > 0)
{
result += CharArrayToString(response, 0, len); // NB: không có CP_UTF8 chỉ 'HEAD'
const int p = StringFind(result, "\r\n\r\n");
if(p > 0)
{
// Tiêu đề HTTP kết thúc bằng hai dòng mới, sử dụng điều này
// để đảm bảo toàn bộ tiêu đề được nhận
Print("HTTP-header found");
StringSetLength(result, p); // cắt bỏ phần thân của tài liệu (trong trường hợp yêu cầu GET)
return true;
}
}
}
while(GetTickCount() - start < timeout && !IsStopped() && !_LastError);
if(_LastError) PRTF(_LastError);
return StringLen(result) > 0;
}
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
Ở đây chúng ta kiểm tra trong một vòng lặp sự xuất hiện của dữ liệu trong thời gian chờ được chỉ định và đọc nó vào bộ đệm response
. Sự xuất hiện của lỗi sẽ chấm dứt vòng lặp.
Các byte bộ đệm được chuyển đổi ngay lập tức thành chuỗi và nối vào phản hồi đầy đủ trong biến result
. Điều quan trọng cần lưu ý là chúng ta chỉ có thể sử dụng hàm CharArrayToString
với mã hóa mặc định cho tiêu đề HTTP vì chỉ các chữ cái Latinh và một vài ký tự đặc biệt từ ANSI được phép trong đó.
Để nhận một tài liệu web hoàn chỉnh, thường có mã hóa UTF-8 (nhưng có thể có mã hóa không Latinh khác, được chỉ định ngay trong tiêu đề HTTP), sẽ yêu cầu xử lý phức tạp hơn: đầu tiên, bạn cần thu thập tất cả các khối đã gửi vào một bộ đệm chung rồi sau đó chuyển đổi toàn bộ thành chuỗi chỉ định CP_UTF8 (nếu không, bất kỳ ký tự nào được mã hóa trong hai byte có thể bị "cắt" khi gửi và sẽ đến trong các khối khác nhau; đó là lý do tại sao chúng ta không thể mong đợi một luồng byte UTF-8 chính xác trong từng đoạn riêng lẻ). Chúng ta sẽ cải thiện ví dụ này trong các phần sau.
Có các hàm HTTPSend
và HTTPRecv
, chúng ta hoàn thiện mã OnStart
.
void OnStart()
{
...
if(PRTF(HTTPSend(socket, StringFormat("HEAD / HTTP/1.1\r\nHost: %s \r\n"
"User-Agent: MetaTrader 5\r\n\r\n", Server))))
{
string response;
if(PRTF(HTTPRecv(socket, response, 5000)))
{
Print(response);
}
}
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Trong tiêu đề HTTP nhận được từ máy chủ, các dòng sau có thể đáng chú ý:
Content-Length:
— tổng chiều dài của tài liệu tính bằng byteContent-Language:
— ngôn ngữ tài liệu (ví dụ, "de-DE, ru")Content-Type:
— mã hóa tài liệu (ví dụ, "text/html; charset=UTF-8")Last-Modified:
— thời gian sửa đổi cuối cùng của tài liệu, để không tải lại những gì đã có (về nguyên tắc, chúng ta có thể thêm tiêu đềIf-Modified-Since:
trong yêu cầu HTTP của mình)
Chúng ta sẽ nói chi tiết hơn về việc tìm hiểu độ dài tài liệu (kích thước dữ liệu) vì hầu hết các tiêu đề là tùy chọn, tức là chúng được máy chủ báo cáo theo ý muốn, và khi không có chúng, các cơ chế thay thế được sử dụng. Kích thước rất quan trọng để biết khi nào nên đóng kết nối, tức là để đảm bảo rằng tất cả dữ liệu đã được nhận.
Chạy script với các tham số mặc định tạo ra kết quả sau.
Server=www.mql5.com / ok
Port=80 / ok
SocketCreate()=1 / ok
SocketConnect(socket,Server,Port,5000)=true / ok
HTTPSend(socket,StringFormat(HEAD / HTTP/1.1
Host: %s
,Server))=true / ok
HTTP-header found
HTTPRecv(socket,response,5000)=true / ok
HTTP/1.1 301 Moved Permanently
Server: nginx
Date: Sun, 31 Jul 2022 10:24:00 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive
Location: https://www.mql5.com/
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: SAMEORIGIN
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vui lòng lưu ý rằng trang web này, giống như hầu hết các trang web ngày nay, chuyển hướng yêu cầu của chúng ta đến một kết nối bảo mật: điều này đạt được bằng mã trạng thái "301 Moved Permanently" và địa chỉ mới "Location: https://www.mql5.com/" (giao thức "https" rất quan trọng ở đây). Để thử lại một yêu cầu hỗ trợ TLS, cần sử dụng một số hàm khác, và chúng ta sẽ thảo luận về chúng sau.