Dịch vụ tín hiệu giao dịch và trang web thử nghiệm
Dịch vụ tín hiệu giao dịch về mặt kỹ thuật giống hệt với dịch vụ chat, tuy nhiên, người dùng của nó (hay đúng hơn là các kết nối máy khách) phải thực hiện một trong hai vai trò:
- Nhà cung cấp tin nhắn
- Người tiêu dùng tin nhắn
Ngoài ra, thông tin không nên доступ cho tất cả mọi người mà cần hoạt động theo một mô hình đăng ký nào đó.
Để đảm bảo điều này, khi kết nối với dịch vụ, người dùng sẽ được yêu cầu cung cấp một số thông tin định danh nhất định, khác nhau tùy thuộc vào vai trò.
Nhà cung cấp phải chỉ định một định danh tín hiệu công khai (PUB_ID
) duy nhất trong số tất cả các tín hiệu. Về cơ bản, một người có thể tạo ra nhiều tín hiệu khác nhau và do đó cần có khả năng lấy được nhiều định danh. Theo nghĩa này, chúng ta sẽ không làm phức tạp dịch vụ bằng cách đưa vào các định danh riêng biệt cho nhà cung cấp (như một cá nhân cụ thể) và định danh của các tín hiệu của họ. Thay vào đó, chỉ hỗ trợ định danh tín hiệu. Đối với một dịch vụ tín hiệu thực sự, vấn đề này cần được giải quyết, cùng với việc xác thực, điều mà chúng ta đã để ngoài cuốn sách này.
Định danh sẽ được yêu cầu để quảng bá nó hoặc đơn giản là truyền nó cho những người quan tâm đến việc đăng ký tín hiệu này. Nhưng không phải "bất kỳ ai bạn gặp" đều có thể truy cập tín hiệu chỉ bằng cách biết định danh công khai. Trong trường hợp đơn giản nhất, điều này có thể chấp nhận được cho việc giám sát tài khoản công khai, nhưng chúng ta sẽ thể hiện tùy chọn hạn chế truy cập cụ thể trong bối cảnh tín hiệu.
Để thực hiện điều này, nhà cung cấp phải cung cấp cho máy chủ một khóa bí mật (PUB_KEY
) chỉ họ biết và không công khai. Khóa này sẽ được yêu cầu để tạo ra khóa truy cập của một người đăng ký cụ thể.
Người tiêu dùng (người đăng ký) cũng phải có một định danh duy nhất (SUB_ID
, và ở đây chúng ta cũng sẽ không sử dụng xác thực). Để đăng ký tín hiệu mong muốn, người dùng phải thông báo định danh này cho nhà cung cấp tín hiệu (trong thực tế, hiểu rằng ở cùng giai đoạn này, cần xác nhận thanh toán, và thường điều này được tự động hóa bởi máy chủ). Nhà cung cấp tạo ra một bản chụp gồm định danh của nhà cung cấp, định danh của người đăng ký và khóa bí mật. Trong dịch vụ của chúng ta, điều này sẽ được thực hiện bằng cách tính toán hàm băm SHA256 từ chuỗi PUB_ID:PUB_KEY:SUB_ID
, sau đó các byte kết quả được chuyển đổi thành chuỗi định dạng thập lục phân. Đây sẽ là khóa truy cập (SUB_KEY
hoặc ACCESS_KEY
) vào tín hiệu của một nhà cung cấp cụ thể cho một người đăng ký cụ thể. Nhà cung cấp (và trong các hệ thống thực tế, máy chủ tự động) chuyển tiếp khóa này cho người đăng ký.
Do đó, khi kết nối với dịch vụ, người đăng ký sẽ phải chỉ định định danh người đăng ký (SUB_ID
), định danh của tín hiệu mong muốn (PUB_ID
) và khóa truy cập (SUB_KEY
). Vì máy chủ biết khóa bí mật của nhà cung cấp, nó có thể tính toán lại khóa truy cập cho tổ hợp PUB_ID
và SUB_ID
đã cho, và so sánh với SUB_KEY
được cung cấp. Nếu khớp, quá trình nhắn tin bình thường sẽ tiếp tục. Sự khác biệt sẽ dẫn đến thông báo lỗi và ngắt kết nối người đăng ký giả mạo khỏi dịch vụ.
Điều quan trọng cần lưu ý là trong bản demo của chúng ta, để đơn giản, không có đăng ký người dùng và tín hiệu bình thường, do đó việc chọn định danh là tùy ý. Điều quan trọng đối với chúng ta chỉ là theo dõi tính duy nhất của các định danh để biết gửi thông tin trực tuyến cho ai và từ ai. Vì vậy, dịch vụ của chúng ta không đảm bảo rằng định danh, ví dụ như Super Trend
, thuộc về cùng một người dùng hôm qua, hôm nay và ngày mai. Việc đặt trước tên được thực hiện theo nguyên tắc ai đến trước được phục vụ trước. Miễn là một nhà cung cấp liên tục kết nối dưới định danh đã cho, tín hiệu sẽ được phân phối. Nếu nhà cung cấp ngắt kết nối, thì định danh đó sẽ trở nên khả dụng để chọn trong bất kỳ kết nối tiếp theo nào.
Định danh duy nhất luôn bận sẽ là Server
: máy chủ sử dụng nó để gửi các thông báo trạng thái kết nối của mình.
Để tạo khóa truy cập trong thư mục máy chủ, có một tệp JavaScript đơn giản access.js
. Khi bạn chạy nó trên dòng lệnh, bạn cần truyền vào một tham số duy nhất là chuỗi kiểu PUB_ID:PUB_KEY:SUB_ID
(các định danh và khóa bí mật giữa chúng, được nối bằng ký hiệu :
).
Nếu tham số không được chỉ định, kịch bản sẽ tạo ra một khóa truy cập cho một số định danh demo (PUB_ID_001
, SUB_ID_100
) và một bí mật (PUB_KEY_FFF
).
// JavaScript
const args = process.argv.slice(2);
const input = args.length > 0 ? args[0] : `PUB_ID_001:PUB_KEY_FFF:SUB_ID_100`;
console.log('Hashing "', input, '"');
const crypto = require(`crypto`);
console.log(crypto.createHash(`sha256`).update(input).digest(`hex`));
2
3
4
5
6
Chạy kịch bản với lệnh:
node access.js PUB_ID_001:PUB_KEY_FFF:SUB_ID_100
chúng ta nhận được kết quả sau:
fd3f7a105eae8c2d9afce0a7a4e11bf267a40f04b7c216dd01cf78c7165a2a5a
Nhân tiện, bạn có thể kiểm tra và lặp lại thuật toán này trong MQL5 thuần túy bằng cách sử dụng hàm CryptEncode
.
Sau khi phân tích phần khái niệm, hãy tiến hành triển khai thực tế.
Kịch bản máy chủ của dịch vụ tín hiệu sẽ được đặt trong tệp MQL5/Experts/MQL5Book/p7/Web/wspubsub.js
. Việc thiết lập máy chủ trong đó giống như những gì chúng ta đã làm trước đây. Tuy nhiên, ngoài ra, bạn sẽ cần kết nối cùng mô-đun crypto
đã được sử dụng trong access.js
. Trang chủ sẽ được gọi là wspubsub.htm
.
// JavaScript
const crypto = require(`crypto`);
...
http1.createServer(options, function (req, res)
{
...
if(req.url == '/')
{
req.url = "wspubsub.htm";
}
...
});
2
3
4
5
6
7
8
9
10
11
12
Thay vì một bản đồ của các máy khách đã kết nối, chúng ta sẽ định nghĩa hai bản đồ, riêng biệt cho nhà cung cấp tín hiệu và người tiêu dùng.
// JavaScript
const publishers = new Map();
const subscribers = new Map();
2
3
Trong cả hai bản đồ, khóa là ID của nhà cung cấp, nhưng bản đồ đầu tiên lưu trữ các đối tượng của nhà cung cấp, còn bản đồ thứ hai lưu trữ các đối tượng của người đăng ký đã đăng ký với mỗi nhà cung cấp (mảng các đối tượng).
Để truyền định danh và khóa trong quá trình bắt tay, chúng ta sẽ sử dụng một tiêu đề đặc biệt được phép bởi đặc tả WebSockets, cụ thể là Sec-Websocket-Protocol
. Hãy đồng ý rằng các định danh và khóa sẽ được nối với nhau bằng ký hiệu -
: trong trường hợp của nhà cung cấp, một chuỗi như X-MQL5-publisher-PUB_ID-PUB_KEY
được mong đợi, và trong trường hợp của người đăng ký, chúng ta mong đợi X-MQL5-subscriber-SUB_ID-PUB_ID-SUB_KEY
.
Bất kỳ nỗ lực nào để kết nối với dịch vụ của chúng ta mà không có tiêu đề Sec-Websocket-Protocol: X-MQL5-...
sẽ bị dừng lại bằng cách đóng ngay lập tức.
Trong đối tượng máy khách mới (trong tham số xử lý sự kiện onConnect(client)
), tiêu đề này dễ dàng được trích xuất từ thuộc tính client.protocol
.
Hãy thể hiện quy trình đăng ký và gửi tin nhắn của nhà cung cấp tín hiệu ở dạng đơn giản hóa, không xử lý lỗi (mã đầy đủ được đính kèm). Điều quan trọng cần lưu ý là văn bản tin nhắn được tạo ở định dạng JSON (chúng ta sẽ thảo luận chi tiết hơn trong phần tiếp theo). Cụ thể, người gửi tin nhắn được truyền trong thuộc tính origin
(hơn nữa, khi tin nhắn được gửi bởi chính dịch vụ, trường này chứa chuỗi Server
), và dữ liệu ứng dụng từ nhà cung cấp được đặt trong thuộc tính msg
, và đây có thể không chỉ là văn bản mà còn là cấu trúc lồng nhau của bất kỳ nội dung nào.
// JavaScript
const wsServer = new WebSocket.Server({ server });
wsServer.on(`connection`, function onConnect(client)
{
console.log(`New user:`, ++count, client.protocol);
if(client.protocol.startsWith(`X-MQL5-publisher`))
{
const parts = client.protocol.split('-');
client.id = parts[3];
client.key = parts[4];
publishers.set(client.id, client);
client.send('{"origin":"Server", "msg":"Hello, publisher ' + client.id + '"}');
client.on(`message`, function(message)
{
console.log('%s : %s', client.id, message);
if(subscribers.get(client.id))
subscribers.get(client.id).forEach(function(elem)
{
elem.send('{"origin":"publisher ' + client.id + '", "msg":'
+ message + '}');
});
});
client.on(`close`, function()
{
console.log(`Publisher disconnected:`, client.id);
if(subscribers.get(client.id))
subscribers.get(client.id).forEach(function(elem)
{
elem.close();
});
publishers.delete(client.id);
});
}
...
});
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
Nửa thuật toán cho người đăng ký tương tự, nhưng ở đây chúng ta có thêm việc tính toán khóa truy cập và so sánh nó với những gì máy khách kết nối đã truyền.
// JavaScript
else if(client.protocol.startsWith(`X-MQL5-subscriber`))
{
const parts = client.protocol.split('-');
client.id = parts[3];
client.pub_id = parts[4];
client.access = parts[5];
const id = client.pub_id;
var p = publishers.get(id);
if(p)
{
const check = crypto.createHash(`sha256`).update(id + ':' + p.key + ':'
+ client.id).digest(`hex`);
if(check != client.access)
{
console.log(`Bad credentials: '${client.access}' vs '${check}'`);
client.send('{"origin":"Server", "msg":"Bad credentials, subscriber '
+ client.id + '"}');
client.close();
return;
}
var list = subscribers.get(id);
if(list == undefined)
{
list = [];
}
list.push(client);
subscribers.set(id, list);
client.send('{"origin":"Server", "msg":"Hello, subscriber '
+ client.id + '"}');
p.send('{"origin":"Server", "msg":"New subscriber ' + client.id + '"}');
}
client.on(`close`, function()
{
console.log(`Subscriber disconnected:`, client.id);
const list = subscribers.get(client.pub_id);
if(list)
{
if(list.length > 1)
{
const filtered = list.filter(function(el) { return el !== client; });
subscribers.set(client.pub_id, filtered);
}
else
{
subscribers.delete(client.pub_id);
}
}
});
}
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
Giao diện người dùng trên trang máy khách wspubsub.htm
chỉ đơn giản mời bạn theo liên kết đến một trong hai trang có biểu mẫu cho nhà cung cấp (wspublisher.htm + wspublisher_client.js
) hoặc người đăng ký (wssubscriber.htm + wssubscriber_client.js
).
Trang web của các máy khách thử nghiệm dịch vụ tín hiệu
Việc triển khai của chúng kế thừa các tính năng của các máy khách JavaScript đã xem xét trước đó, nhưng liên quan đến việc tùy chỉnh tiêu đề Sec-Websocket-Protocol: X-MQL5-
và một sắc thái nữa.
Cho đến nay, chúng ta đã trao đổi các tin nhắn văn bản đơn giản. Nhưng đối với một dịch vụ tín hiệu, bạn sẽ cần truyền nhiều thông tin có cấu trúc, và JSON phù hợp hơn cho việc này. Do đó, các máy khách có thể phân tích cú pháp JSON, mặc dù chúng không sử dụng nó đúng mục đích, vì ngay cả khi một lệnh mua hoặc bán một mã cụ thể với số lượng nhất định được tìm thấy trong JSON, trình duyệt không biết cách thực hiện điều này.
Chúng ta sẽ cần thêm hỗ trợ JSON vào máy khách dịch vụ tín hiệu của chúng ta trong MQL5. Trong khi đó, bạn có thể chạy trên máy chủ wspubsub.js
và kiểm tra kết nối chọn lọc của các nhà cung cấp tín hiệu và người tiêu dùng theo các chi tiết do họ chỉ định. Chúng ta khuyên bạn tự làm điều này, vì lợi ích của chính bạn.