Giao thức WebSocket trong MQL5
Chúng ta đã xem xét trước đây về Cơ sở lý thuyết của giao thức WebSockets. Đặc tả đầy đủ khá rộng lớn, và một mô tả chi tiết về việc triển khai nó sẽ đòi hỏi rất nhiều không gian và thời gian. Do đó, chúng ta trình bày cấu trúc chung của các lớp đã hoàn thiện và giao diện lập trình của chúng. Tất cả các tệp đều nằm trong thư mục MQL5/Include/MQL5Book/ws/
.
wsinterfaces.mqh
— mô tả trừu tượng chung của tất cả các giao diện, hằng số và kiểu;wstransport.mqh
— lớpMqlWebSocketTransport
thực hiện giao diện truyền dữ liệu mạng cấp thấpIWebSocketTransport
dựa trên các hàm socket của MQL5;wsframe.mqh
— các lớpWebSocketFrame
vàWebSocketFrameHixie
thực hiện giao diệnIWebSocketFrame
, ẩn các thuật toán tạo (mã hóa và giải mã) khung cho giao thức Hybi và Hixie tương ứng;wsmessage.mqh
— các lớpWebSocketMessage
vàWebSocketMessageHixie
thực hiện giao diệnIWebSocketMessage
, chuẩn hóa việc hình thành các tin nhắn từ các khung cho giao thức Hybi và Hixie tương ứng;wsprotocol.mqh
— các lớpWebSocketConnection
,WebSocketConnectionHybi
,WebSocketConnectionHixie
kế thừa từIWebSocketConnection
; tại đây diễn ra việc quản lý phối hợp việc tạo khung, tin nhắn, lời chào và ngắt kết nối theo đặc tả, sử dụng các giao diện trên;wsclient.mqh
— triển khai sẵn sàng của một máy khách WebSocket; lớp mẫuWebSocketClient
hỗ trợ giao diệnIWebSocketObserver
(để xử lý sự kiện) và mong đợiWebSocketConnectionHybi
hoặcWebSocketConnectionHixie
làm kiểu tham số hóa;wstools.mqh
— các tiện ích hữu ích trong không gian tênWsTools
.
Các tệp tiêu đề này sẽ được tự động bao gồm trong các dự án mqporj tương lai của chúng ta dưới dạng phụ thuộc từ các chỉ thị #include
.
Sơ đồ lớp WebSocket trong MQL5
Giao diện mạng cấp thấp IWebSocketTransport
có các phương thức sau.
interface IWebSocketTransport
{
int write(const uchar &data[]); // ghi mảng byte vào mạng
int read(uchar &buffer[]); // đọc dữ liệu từ mạng vào mảng byte
bool isConnected(void) const; // kiểm tra kết nối
bool isReadable(void) const; // kiểm tra khả năng đọc từ mạng
bool isWritable(void) const; // kiểm tra khả năng ghi vào mạng
int getHandle(void) const; // mô tả socket hệ thống
void close(void); // đóng kết nối
};
2
3
4
5
6
7
8
9
10
Không khó để đoán từ tên các phương thức rằng các hàm Socket API của MQL5 sẽ được sử dụng để xây dựng chúng. Nhưng nếu cần, những người muốn có thể tự triển khai giao diện này bằng phương tiện của riêng họ, ví dụ, thông qua DLL.
Lớp MqlWebSocketTransport
thực hiện giao diện này yêu cầu giao thức, tên máy chủ và số cổng mà kết nối mạng được thực hiện khi tạo một thể hiện. Ngoài ra, bạn có thể chỉ định giá trị thời gian chờ.
Các loại khung được thu thập trong enum WS_FRAME_OPCODE
.
enum WS_FRAME_OPCODE
{
WS_DEFAULT = 0,
WS_CONTINUATION_FRAME = 0x00,
WS_TEXT_FRAME = 0x01,
WS_BINARY_FRAME = 0x02,
WS_CLOSE_FRAME = 0x08,
WS_PING_FRAME = 0x09,
WS_PONG_FRAME = 0x0A
};
2
3
4
5
6
7
8
9
10
Giao diện làm việc với các khung chứa cả phương thức tĩnh và phương thức thông thường liên quan đến các thể hiện khung. Các phương thức tĩnh đóng vai trò như nhà máy để tạo các khung có loại cần thiết bởi phía truyền (create
) và các khung đến (decode
).
class IWebSocketFrame
{
public:
class StaticCreator
{
public:
virtual IWebSocketFrame *decode(uchar &data[], IWebSocketFrame *head = NULL) = 0;
virtual IWebSocketFrame *create(WS_FRAME_OPCODE type, const string data = NULL,
const bool deflate = false) = 0;
virtual IWebSocketFrame *create(WS_FRAME_OPCODE type, const uchar &data[],
const bool deflate = false) = 0;
};
...
2
3
4
5
6
7
8
9
10
11
12
13
Sự hiện diện của các phương thức nhà máy trong các lớp con được bắt buộc do sự tồn tại của mẫu Creator
và một thể hiện của phương thức getCreator
trả về nó (giả định trả về "singleton").
protected:
template<typename P>
class Creator: public StaticCreator
{
public:
// giải mã dữ liệu nhị phân nhận được trong IWebSocketFrame
// (trong trường hợp tiếp tục, khung trước đó trong 'head')
virtual IWebSocketFrame *decode(uchar &data[],
IWebSocketFrame *head = NULL) override
{
return P::decode(data, head);
}
// tạo một khung có loại mong muốn (văn bản/đóng/khác) với văn bản tùy chọn
virtual IWebSocketFrame *create(WS_FRAME_OPCODE type, const string data = NULL,
const bool deflate = false) override
{
return P::create(type, data, deflate);
};
// tạo một khung có loại mong muốn (nhị phân/văn bản/đóng/khác) với dữ liệu
virtual IWebSocketFrame *create(WS_FRAME_OPCODE type, const uchar &data[],
const bool deflate = false) override
{
return P::create(type, data, deflate);
};
};
public:
// yêu cầu một thể hiện Creator
virtual IWebSocketFrame::StaticCreator *getCreator() = 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
Các phương thức còn lại của giao diện cung cấp tất cả các thao tác cần thiết với dữ liệu trong các khung (mã hóa/giải mã, nhận dữ liệu và các cờ khác nhau).
// mã hóa nội dung "sạch" của khung thành dữ liệu để truyền qua mạng
virtual int encode(uchar &encoded[]) = 0;
// lấy dữ liệu dưới dạng văn bản
virtual string getData() = 0;
// lấy dữ liệu dưới dạng byte, trả về kích thước
virtual int getData(uchar &buf[]) = 0;
// trả về loại khung (opcode)
virtual WS_FRAME_OPCODE getType() = 0;
// kiểm tra xem khung có phải là khung điều khiển hay chứa dữ liệu:
// các khung điều khiển được xử lý bên trong các lớp
virtual bool isControlFrame()
{
return (getType() >= WS_CLOSE_FRAME);
}
virtual bool isReady() { return true; }
virtual bool isFinal() { return true; }
virtual bool isMasked() { return false; }
virtual bool isCompressed() { return false; }
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Giao diện IWebSocketMessage
chứa các phương thức để thực hiện các hành động tương tự nhưng ở cấp độ tin nhắn.
class IWebSocketMessage
{
public:
// lấy mảng các khung tạo nên tin nhắn này
virtual void getFrames(IWebSocketFrame *&frames[]) = 0;
// đặt văn bản làm nội dung tin nhắn
virtual bool setString(const string &data) = 0;
// trả về nội dung tin nhắn dưới dạng văn bản
virtual string getString() = 0;
// đặt dữ liệu nhị phân làm nội dung tin nhắn
virtual bool setData(const uchar &data[]) = 0;
// trả về nội dung của tin nhắn ở dạng nhị phân "thô"
virtual bool getData(uchar &data[]) = 0;
// dấu hiệu hoàn tất của tin nhắn (tất cả các khung đã nhận được)
virtual bool isFinalised() = 0;
// thêm một khung vào tin nhắn
virtual bool takeFrame(IWebSocketFrame *frame) = 0;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Cân nhắc các giao diện của khung và tin nhắn, một giao diện chung cho các kết nối WebSocket IWebSocketConnection
được định nghĩa.
interface IWebSocketConnection
{
// mở kết nối với URL được chỉ định và các phần của nó,
// và các tiêu đề tùy chỉnh tùy chọn
bool handshake(const string url, const string host, const string origin,
const string custom = NULL);
// đọc khung cấp thấp từ máy chủ
int readFrame(IWebSocketFrame *&frames[]);
// gửi khung cấp thấp (ví dụ: đóng hoặc ping)
bool sendFrame(IWebSocketFrame *frame);
// gửi tin nhắn cấp thấp
bool sendMessage(IWebSocketMessage *msg);
// kiểm tra tùy chỉnh cho các tin nhắn mới (tạo sự kiện)
int checkMessages();
// gửi văn bản tùy chỉnh
bool sendString(const string msg);
// gửi dữ liệu nhị phân tùy chỉnh
bool sendData(const uchar &data[]);
// đóng kết nối
bool disconnect(void);
};
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
Thông báo về ngắt kết nối và tin nhắn mới được nhận qua các phương thức của giao diện IWebSocketObserver
.
interface IWebSocketObserver
{
void onConnected();
void onDisconnect();
void onMessage(IWebSocketMessage *msg);
};
2
3
4
5
6
Đặc biệt, lớp WebSocketClient
được làm kế thừa từ giao diện này và mặc định chỉ xuất thông tin ra nhật ký. Hàm tạo của lớp mong đợi một địa chỉ để kết nối với giao thức ws
hoặc wss
.
template<typename T>
class WebSocketClient: public IWebSocketObserver
{
protected:
IWebSocketMessage *messages[];
string scheme;
string host;
string port;
string origin;
string url;
int timeOut;
...
public:
WebSocketClient(const string address)
{
string parts[];
URL::parse(address, parts);
url = address;
timeOut = 5000;
scheme = parts[URL_SCHEME];
if(scheme != "ws" && scheme != "wss")
{
Print("WebSocket invalid url scheme: ", scheme);
scheme = "ws";
}
host = parts[URL_HOST];
port = parts[URL_PORT];
origin = (scheme == "wss" ? "https://" : "http://") + host;
}
...
void onDisconnect() override
{
Print(" > Disconnected ", url);
}
void onConnected() override
{
Print(" > Connected ", url);
}
void onMessage(IWebSocketMessage *msg) override
{
// NB: tin nhắn có thể là nhị phân, in nó chỉ để thông báo
Print(" > Message ", url, " " , msg.getString());
WsTools::push(messages, msg);
}
...
};
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
Lớp WebSocketClient
thu thập tất cả các đối tượng tin nhắn vào một mảng và đảm nhận việc xóa chúng nếu chương trình MQL không thực hiện.
Kết nối được thiết lập trong phương thức open
.
template<typename T>
class WebSocketClient: public IWebSocketObserver
{
protected:
IWebSocketTransport *socket;
IWebSocketConnection *connection;
...
public:
...
bool open(const string custom_headers = NULL)
{
uint _port = (uint)StringToInteger(port);
if(_port == 0)
{
if(scheme == "ws") _port = 80;
else _port = 443;
}
socket = MqlWebSocketTransport::create(scheme, host, _port, timeOut);
if(!socket || !socket.isConnected())
{
return false;
}
connection = new T(&this, socket);
return connection.handshake(url, host, origin, custom_headers);
}
...
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
Các cách thuận tiện nhất để gửi dữ liệu được cung cấp bởi các phương thức send
quá tải cho dữ liệu văn bản và nhị phân.
bool send(const string str)
{
return connection ? connection.sendString(str) : false;
}
bool send(const uchar &data[])
{
return connection ? connection.sendData(data) : false;
}
2
3
4
5
6
7
8
9
Để kiểm tra các tin nhắn đến mới, bạn có thể gọi phương thức checkMessages
. Tùy thuộc vào tham số blocking
của nó, phương thức sẽ đợi tin nhắn trong một vòng lặp cho đến khi hết thời gian chờ hoặc trả về ngay lập tức nếu không có tin nhắn. Tin nhắn sẽ được gửi đến trình xử lý IWebSocketObserver::onMessage
.
void checkMessages(const bool blocking = true)
{
if(connection == NULL) return;
uint stop = GetTickCount() + (blocking ? timeOut : 1);
while(ArraySize(messages) == 0 && GetTickCount() < stop && isConnected())
{
// tất cả các khung được thu thập vào các tin nhắn thích hợp, và chúng trở nên
// khả dụng thông qua thông báo sự kiện IWebSocketObserver::onMessage,
// tuy nhiên, các khung điều khiển đã được xử lý nội bộ và loại bỏ vào lúc này
if(!connection.checkMessages()) // trong khi không có tin nhắn, hãy tạm dừng nhỏ
{
Sleep(100);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Một cách thay thế để nhận tin nhắn được triển khai trong phương thức readMessage
: nó trả về một con trỏ đến tin nhắn cho mã gọi (nói cách khác, trình xử lý ứng dụng onMessage
không bắt buộc). Sau đó, chương trình MQL chịu trách nhiệm giải phóng đối tượng.
IWebSocketMessage *readMessage(const bool blocking = true)
{
if(ArraySize(messages) == 0) checkMessages(blocking);
if(ArraySize(messages) > 0)
{
IWebSocketMessage *top = messages[0];
ArrayRemove(messages, 0, 1);
return top;
}
return NULL;
}
2
3
4
5
6
7
8
9
10
11
12
Lớp cũng cho phép thay đổi thời gian chờ, kiểm tra kết nối và đóng nó.
void setTimeOut(const int ms)
{
timeOut = fabs(ms);
}
bool isConnected() const
{
return socket && socket.isConnected();
}
void close()
{
if(isConnected())
{
if(connection)
{
connection.disconnect(); // điều này sẽ đóng socket sau khi máy chủ xác nhận
delete connection;
connection = NULL;
}
if(socket)
{
delete socket;
socket = NULL;
}
}
}
};
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
Thư viện của các lớp đã xem xét cho phép tạo các ứng dụng khách cho các dịch vụ echo và chat.