Thành phần máy chủ của dịch vụ web dựa trên giao thức WebSocket
Để tổ chức một thành phần máy chủ chung cho tất cả các dự án, chúng ta sẽ tạo một thư mục riêng Web
bên trong MQL5/Experts/MQL5Book/p7/
. Lý tưởng nhất, sẽ thuận tiện khi đặt Web
như một thư mục con trong Shared Projects
. Sự thật là MQL5/Shared Projects
có sẵn trong bản phân phối tiêu chuẩn của MetaTrader 5 và được dành riêng cho các dự án lưu trữ đám mây. Do đó, sau này, bằng cách sử dụng chức năng của các dự án chia sẻ, có thể tải tất cả các tệp của dự án của chúng ta lên máy chủ (không chỉ các tệp web mà còn cả các chương trình MQL).
Sau này, khi chúng ta tạo một tệp mqproj với các chương trình máy khách MQL5, chúng ta sẽ thêm tất cả các tệp trong thư mục này vào phần dự án Settings and Files
, vì tất cả các tệp này tạo thành một phần không thể thiếu của dự án — phần máy chủ.
Vì một thư mục riêng đã được phân bổ cho máy chủ dự án, cần đảm bảo khả năng nhập các mô-đun từ nodejs trong thư mục này. Theo mặc định, nodejs tìm kiếm các mô-đun trong thư mục con /node_modules
của thư mục hiện tại, và chúng ta sẽ chạy máy chủ từ dự án. Do đó, khi đang ở trong thư mục nơi chúng ta sẽ đặt các tệp web của dự án, hãy chạy lệnh:
mklink /j node_modules {drive:/path/to/folder/nodejs}/node_modules
Kết quả là, một liên kết thư mục "tượng trưng" có tên node_modules
sẽ xuất hiện, trỏ đến thư mục gốc cùng tên trong nodejs đã cài đặt.
Cách dễ nhất để kiểm tra chức năng của WebSocket là dịch vụ echo. Mô hình hoạt động của nó là trả lại bất kỳ tin nhắn nào nhận được cho người gửi. Hãy xem xét cách tổ chức một dịch vụ như vậy trong cấu hình tối thiểu. Một ví dụ được bao gồm trong tệp wsintro.js
.
Trước hết, chúng ta kết nối gói (mô-đun) ws
, cung cấp chức năng WebSocket cho nodejs và được cài đặt cùng với máy chủ web.
// JavaScript
const WebSocket = require('ws');
2
Hàm require
hoạt động tương tự như chỉ thị #include
trong MQL5, nhưng ngoài ra còn trả về một đối tượng mô-đun với API của tất cả các tệp trong gói ws
. Nhờ đó, chúng ta có thể gọi các phương thức và thuộc tính của đối tượng WebSocket
. Trong trường hợp này, chúng ta cần tạo một máy chủ WebSocket trên cổng 9000.
// JavaScript
const port = 9000;
const wss = new WebSocket.Server({ port: port });
2
3
Ở đây chúng ta thấy lời gọi hàm tạo thông thường của MQL5 bởi toán tử new
, nhưng một đối tượng (cấu trúc) không tên được truyền dưới dạng tham số, trong đó, như trong một bản đồ, có thể lưu trữ một tập hợp các thuộc tính được đặt tên và giá trị của chúng. Trong trường hợp này, chỉ sử dụng một thuộc tính port
, và giá trị của nó được đặt bằng biến (chính xác hơn là hằng số) port
đã mô tả ở trên. Về cơ bản, chúng ta có thể truyền số cổng (và các cài đặt khác) trên dòng lệnh khi chạy tập lệnh.
Đối tượng máy chủ được đưa vào biến wss
. Khi thành công, chúng ta báo hiệu cho cửa sổ dòng lệnh rằng máy chủ đang chạy (chờ kết nối).
// JavaScript
console.log('listening on port: ' + port);
2
Lời gọi console.log
tương tự như Print
thông thường trong MQL5. Cũng lưu ý rằng các chuỗi trong JavaScript có thể được bao quanh không chỉ bởi dấu nháy kép mà còn bởi dấu nháy đơn, và thậm chí cả dấu nháy ngược `this is a ${template}text`
, điều này bổ sung một số tính năng hữu ích.
Tiếp theo, đối với đối tượng wss
, chúng ta gán một trình xử lý sự kiện "connection", liên quan đến việc kết nối của một máy khách mới. Rõ ràng, danh sách các sự kiện được hỗ trợ của đối tượng được xác định bởi các nhà phát triển của gói, trong trường hợp này là gói ws
mà chúng ta sử dụng. Tất cả điều này được phản ánh trong tài liệu.
Trình xử lý được gắn kết bởi phương thức on
, chỉ định tên của sự kiện và chính trình xử lý.
// JavaScript
wss.on('connection', function(channel)
{
...
});
2
3
4
5
Trình xử lý là một hàm không tên (ẩn danh) được định nghĩa trực tiếp tại nơi mà một tham số tham chiếu được mong đợi cho mã gọi lại sẽ được thực thi khi có kết nối mới. Hàm được làm ẩn danh vì nó chỉ được sử dụng tại đây, và JavaScript cho phép những đơn giản hóa như vậy trong cú pháp. Hàm chỉ có một tham số là đối tượng của kết nối mới. Chúng ta tự do chọn tên cho tham số, và trong trường hợp này, nó là channel
.
Bên trong trình xử lý, một trình xử lý khác nên được đặt cho sự kiện "message" liên quan đến việc đến của một tin nhắn mới trong một kênh cụ thể.
// JavaScript
channel.on('message', function(message)
{
console.log('message: ' + message);
channel.send('echo: ' + message);
});
...
2
3
4
5
6
7
Nó cũng sử dụng một hàm ẩn danh với một tham số duy nhất, đối tượng tin nhắn nhận được. Chúng ta in nó ra nhật ký điều khiển để gỡ lỗi. Nhưng điều quan trọng nhất xảy ra ở dòng thứ hai: bằng cách gọi channel.send
, chúng ta gửi một tin nhắn phản hồi đến máy khách.
Để hoàn thiện bức tranh, hãy thêm tin nhắn chào mừng của riêng chúng ta vào trình xử lý "connection". Khi hoàn tất, nó trông như thế này:
// JavaScript
wss.on('connection', function(channel)
{
channel.on('message', function(message)
{
console.log('message: ' + message);
channel.send('echo: ' + message);
});
console.log('new client connected!');
channel.send('connected!');
});
2
3
4
5
6
7
8
9
10
11
Điều quan trọng là phải hiểu rằng trong khi việc gắn kết trình xử lý "message" cao hơn trong mã so với việc gửi lời chào, trình xử lý tin nhắn sẽ được gọi sau đó, và chỉ khi máy khách gửi một tin nhắn.
Chúng ta đã xem xét một phác thảo tập lệnh để tổ chức dịch vụ echo. Tuy nhiên, sẽ tốt nếu thử nghiệm nó. Điều này có thể được thực hiện một cách hiệu quả nhất bằng cách sử dụng một trình duyệt thông thường, nhưng điều này sẽ đòi hỏi làm phức tạp tập lệnh một chút: biến nó thành máy chủ web nhỏ nhất có thể, trả về một trang web với máy khách WebSocket nhỏ nhất có thể.
Dịch vụ Echo và trang web thử nghiệm
Tập lệnh máy chủ echo mà chúng ta sẽ xem xét bây giờ nằm trong tệp wsecho.js
. Một trong những điểm chính là mong muốn hỗ trợ không chỉ các giao thức mở trên máy chủ http/ws
mà còn các giao thức được bảo vệ https/wss
. Khả năng này sẽ được cung cấp trong tất cả các ví dụ của chúng ta (bao gồm cả máy khách dựa trên MQL5), nhưng để làm điều này, bạn cần thực hiện một số hành động trên máy chủ.
Bạn nên bắt đầu với một cặp tệp chứa khóa mã hóa và chứng chỉ. Các tệp này thường được lấy từ các nguồn được ủy quyền, tức là các trung tâm chứng nhận, nhưng vì mục đích thông tin, bạn có thể tự tạo các tệp này. Tất nhiên, chúng không thể được sử dụng trên các máy chủ công cộng, và các trang có chứng chỉ như vậy sẽ gây ra cảnh báo trong bất kỳ trình duyệt nào (biểu tượng trang bên trái thanh địa chỉ được đánh dấu màu đỏ).
Mô tả về cấu trúc của chứng chỉ và quy trình tự tạo chúng vượt ra ngoài phạm vi của cuốn sách, nhưng hai tệp đã sẵn sàng được bao gồm trong sách: MQL5Book.crt
và MQL5Book.key
(có các phần mở rộng khác) với thời hạn giới hạn. Các tệp này phải được truyền cho hàm tạo của đối tượng máy chủ web để máy chủ hoạt động qua giao thức HTTPS.
Chúng ta sẽ truyền tên của các tệp chứng chỉ trong dòng lệnh khởi chạy tập lệnh. Ví dụ, như sau:
node wsecho.js MQL5Book
Nếu bạn chạy tập lệnh mà không có tham số bổ sung, máy chủ sẽ hoạt động bằng giao thức HTTP.
node wsecho.js
Bên trong tập lệnh, các đối số dòng lệnh có sẵn thông qua đối tượng tích hợp process.argv
, và hai đối số đầu tiên luôn chứa, tương ứng, tên của máy chủ node.exe
và tên của tập lệnh để chạy (trong trường hợp này, wsecho.js
), vì vậy chúng ta loại bỏ chúng bằng phương thức splice
.
// JavaScript
const args = process.argv.slice(2);
const secure = args.length > 0 ? 'https' : 'http';
2
3
Tùy thuộc vào sự hiện diện của tên chứng chỉ, biến secure
nhận tên của gói cần được tải tiếp theo để tạo máy chủ: https
hoặc http
. Tổng cộng, chúng ta có 3 phụ thuộc trong mã:
// JavaScript
const fs = require('fs');
const http1 = require(secure);
const WebSocket = require('ws');
2
3
4
Chúng ta đã biết tất cả về gói ws
; các gói https
và http
cung cấp triển khai máy chủ web, và gói tích hợp fs
cung cấp công việc với hệ thống tệp.
Cài đặt máy chủ web được định dạng dưới dạng đối tượng options
. Ở đây chúng ta thấy cách tên của chứng chỉ từ dòng lệnh được thay thế trong các chuỗi với dấu nháy ngược bằng biểu thức ${args[0]}
. Sau đó, cặp tệp tương ứng được đọc bởi phương thức fs.readFileSync
.
// JavaScript
const options = args.length > 0 ?
{
key : fs.readFileSync(`${args[0]}.key`),
cert : fs.readFileSync(`${args[0]}.crt`)
} : null;
2
3
4
5
6
Máy chủ web được tạo bằng cách gọi phương thức createServer
, mà chúng ta truyền đối tượng options và một hàm ẩn danh — trình xử lý yêu cầu HTTP. Trình xử lý có hai tham số: đối tượng req
với yêu cầu HTTP và đối tượng res
mà chúng ta nên gửi phản hồi (các tiêu đề HTTP và trang web).
// JavaScript
http1.createServer(options, function (req, res)
{
console.log(req.method, req.url);
console.log(req.headers);
if(req.url == '/') req.url = "index.htm";
fs.readFile('./' + req.url, (err, data) =>
{
if(!err)
{
var dotoffset = req.url.lastIndexOf('.');
var mimetype = dotoffset == -1 ? 'text/plain' :
{
'.htm' : 'text/html',
'.html' : 'text/html',
'.css' : 'text/css',
'.js' : 'text/javascript'
}[ req.url.substr(dotoffset) ];
res.setHeader('Content-Type',
mimetype == undefined ? 'text/plain' : mimetype);
res.end(data);
}
else
{
console.log('File not fount: ' + req.url);
res.writeHead(404, "Not Found");
res.end();
}
});
}).listen(secure == 'https' ? 443 : 80);
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
Trang chính (và duy nhất) là index.htm
(sẽ được viết ngay bây giờ). Ngoài ra, trình xử lý có thể gửi các tệp js và css, điều này sẽ hữu ích cho chúng ta trong tương lai. Tùy thuộc vào việc chế độ bảo vệ có được kích hoạt hay không, máy chủ được khởi động bằng cách gọi phương thức listen
trên các cổng tiêu chuẩn 443 hoặc 80 (thay đổi sang các cổng khác nếu những cổng này đã được sử dụng trên máy tính của bạn).
Để chấp nhận các kết nối trên cổng 9000 cho web sockets, chúng ta cần triển khai một phiên bản máy chủ web khác với cùng các tùy chọn. Nhưng trong trường hợp này, máy chủ chỉ có mục đích xử lý yêu cầu HTTP để "nâng cấp" kết nối lên giao thức Web Sockets.
// JavaScript
const server = new http1.createServer(options).listen(9000);
server.on('upgrade', function(req, socket, head)
{
console.log(req.headers); // TODO: we can add authorization!
});
2
3
4
5
6
Ở đây, trong trình xử lý sự kiện "upgrade", chúng ta chấp nhận bất kỳ kết nối nào đã vượt qua quá trình bắt tay và in các tiêu đề vào nhật ký, nhưng tiềm năng chúng ta có thể yêu cầu xác thực người dùng nếu chúng ta đang thực hiện một dịch vụ khép kín (có trả phí).
Cuối cùng, chúng ta tạo một đối tượng máy chủ WebSocket, như trong ví dụ giới thiệu trước đó, với sự khác biệt duy nhất là một máy chủ web đã sẵn sàng được truyền vào hàm tạo. Tất cả các máy khách kết nối được đếm và chào đón theo số thứ tự.
// JavaScript
var count = 0;
const wsServer = new WebSocket.Server({ server });
wsServer.on('connection', function onConnect(client)
{
console.log('New user:', ++count);
client.id = count;
client.send('server#Hello, user' + count);
client.on('message', function(message)
{
console.log('%d : %s', client.id, message);
client.send('user' + client.id + '#' + message);
});
client.on('close', function()
{
console.log('User disconnected:', client.id);
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Đối với tất cả các sự kiện, bao gồm kết nối, ngắt kết nối và tin nhắn, thông tin gỡ lỗi được hiển thị trong bảng điều khiển.
Vậy là, máy chủ web với hỗ trợ máy chủ web socket đã sẵn sàng. Bây giờ chúng ta cần tạo một trang web máy khách index.htm
cho nó.
// HTML
<!DOCTYPE html>
<html>
<head>
<title>Test Server (HTTP[S]/WS[S])</title>
</head>
<body>
<div>
<h1>Test Server (HTTP[S]/WS[S])</h1>
<p><label>
Message: <input id="message" name="message" placeholder="Enter a text">
</label></p>
<p><button>Submit</button> <button>Close</button></p>
<p><label>
Echo: <input id="echo" name="echo" placeholder="Text from server">
</label></p>
</div>
</body>
<script src="wsecho_client.js"></script>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Trang này là một biểu mẫu với một trường nhập duy nhất và một nút để gửi tin nhắn.
Trang web dịch vụ Echo trên WebSocket
Trang sử dụng tập lệnh wsecho_client.js
, cung cấp phản hồi máy khách websocket. Trong các trình duyệt, web sockets được tích hợp sẵn dưới dạng đối tượng JavaScript "tự nhiên", vì vậy bạn không cần kết nối bất cứ thứ gì bên ngoài: chỉ cần gọi hàm tạo web socket
với giao thức mong muốn và số cổng.
// JavaScript
const proto = window.location.protocol.startsWith('http') ?
window.location.protocol.replace('http', 'ws') : 'ws:';
const ws = new WebSocket(proto + '//' + window.location.hostname + ':9000');
2
3
4
URL được hình thành từ địa chỉ của trang web hiện tại (window.location.hostname
), vì vậy kết nối web socket được thực hiện đến cùng một máy chủ.
Tiếp theo, đối tượng ws
cho phép bạn phản ứng với các sự kiện và gửi tin nhắn. Trong trình duyệt, sự kiện mở kết nối được gọi là "open"; nó được kết nối qua thuộc tính onopen
. Cú pháp tương tự, hơi khác so với triển khai máy chủ, cũng được sử dụng cho sự kiện đến của tin nhắn mới — trình xử lý cho nó được gán cho thuộc tính onmessage
.
// JavaScript
ws.onopen = function()
{
console.log('Connected');
};
ws.onmessage = function(message)
{
console.log('Message: %s', message.data);
document.getElementById('echo').value = message.data;
};
2
3
4
5
6
7
8
9
10
11
Văn bản của tin nhắn đến được hiển thị trong phần tử biểu mẫu với id "echo". Lưu ý rằng đối tượng sự kiện tin nhắn (tham số trình xử lý) không phải là tin nhắn, mà có sẵn trong thuộc tính data
. Đây là một đặc điểm triển khai trong JavaScript.
Phản ứng với các nút biểu mẫu được gán bằng phương thức addEventListener
cho mỗi trong hai đối tượng thẻ button
. Ở đây chúng ta thấy một cách khác để mô tả một hàm ẩn danh trong JavaScript: dấu ngoặc với danh sách đối số có thể trống, và thân hàm sau mũi tên có thể là (arguments) => { ... }
.
// JavaScript
const button = document.querySelectorAll('button'); // request all buttons
// button "Submit"
button[0].addEventListener('click', (event) =>
{
const x = document.getElementById('message').value;
if(x) ws.send(x);
});
// button "close"
button[1].addEventListener('click', (event) =>
{
ws.close();
document.getElementById('echo').value = 'disconnected';
Array.from(document.getElementsByTagName('button')).forEach((e) =>
{
e.disabled = true;
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Để gửi tin nhắn, chúng ta gọi phương thức ws.send
, và để đóng kết nối, chúng ta gọi phương thức ws.close
.
Điều này hoàn tất việc phát triển ví dụ đầu tiên về các tập lệnh máy khách-máy chủ để thể hiện dịch vụ echo. Bạn có thể chạy wsecho.js
bằng một trong các lệnh được hiển thị trước đó, sau đó mở trong trình duyệt của bạn trang tại http://localhost
hoặc https://localhost
(tùy thuộc vào cài đặt máy chủ). Sau khi biểu mẫu xuất hiện trên màn hình, hãy thử trò chuyện với máy chủ và đảm bảo rằng dịch vụ đang chạy.
Dần dần làm phức tạp ví dụ này, chúng ta sẽ mở đường cho dịch vụ web để sao chép tín hiệu giao dịch. Nhưng bước tiếp theo sẽ là dịch vụ trò chuyện, nguyên tắc hoạt động của nó tương tự như dịch vụ tín hiệu giao dịch: tin nhắn từ một người dùng được truyền đến những người dùng khác.
Dịch vụ trò chuyện và trang web thử nghiệm
Tập lệnh máy chủ mới được gọi là wschat.js
, và nó lặp lại rất nhiều từ wsecho.js
. Hãy liệt kê các khác biệt chính. Trong trình xử lý yêu cầu HTTP của máy chủ web, thay đổi trang ban đầu từ index.htm
thành wschat.htm
.
// JavaScript
http1.createServer(options, function (req, res)
{
if(req.url == '/') req.url = "wschat.htm";
...
});
2
3
4
5
6
Để lưu trữ thông tin về người dùng kết nối với trò chuyện, chúng ta sẽ mô tả mảng bản đồ clients
. Map
là một bộ chứa liên kết tiêu chuẩn của JavaScript, trong đó các giá trị bất kỳ có thể được ghi bằng các khóa thuộc bất kỳ loại nào, bao gồm cả đối tượng.
// JavaScript
const clients = new Map(); // added this line
var count = 0;
2
3
Trong trình xử lý sự kiện kết nối người dùng mới, chúng ta sẽ thêm đối tượng client
, nhận được dưới dạng tham số hàm, vào bản đồ dưới số thứ tự máy khách hiện tại.
// JavaScript
wsServer.on('connection', function onConnect(client)
{
console.log('New user:', ++count);
client.id = count;
client.send('server#Hello, user' + count);
clients.set(count, client); // added this line
...
2
3
4
5
6
7
8
Bên trong hàm onConnect
, chúng ta đặt một trình xử lý cho sự kiện về việc đến của một tin nhắn mới cho một máy khách cụ thể, và chính bên trong trình xử lý lồng nhau này mà chúng ta gửi tin nhắn. Tuy nhiên, lần này chúng ta lặp qua tất cả các phần tử của bản đồ (tức là qua tất cả các máy khách) và gửi văn bản đến từng người trong số họ. Vòng lặp được tổ chức với các lời gọi phương thức forEach
cho một mảng từ bản đồ, và hàm ẩn danh tiếp theo sẽ được thực hiện cho mỗi phần tử (elem
) được truyền vào phương thức ngay tại chỗ. Ví dụ về vòng lặp này thể hiện rõ ràng mô hình lập trình functional-declarative
chiếm ưu thế trong JavaScript (trái ngược với cách tiếp cận imperative
trong MQL5).
// JavaScript
client.on('message', function(message)
{
console.log('%d : %s', client.id, message);
Array.from(clients.values()).forEach(function(elem) // added a loop
{
elem.send('user' + client.id + '#' + message);
});
});
2
3
4
5
6
7
8
9
Điều quan trọng cần lưu ý là chúng ta gửi một bản sao của tin nhắn đến tất cả các máy khách, bao gồm cả tác giả ban đầu. Nó có thể được lọc ra, nhưng vì mục đích gỡ lỗi, tốt hơn là có xác nhận rằng tin nhắn đã được gửi.
Sự khác biệt cuối cùng so với dịch vụ echo trước đó là khi một máy khách ngắt kết nối, nó cần được xóa khỏi bản đồ.
// JavaScript
client.on('close', function()
{
console.log('User disconnected:', client.id);
clients.delete(client.id); // added this line
});
2
3
4
5
6
Về việc thay thế trang index.htm
bằng wschat.htm
, ở đây chúng ta đã thêm một "trường" để hiển thị tác giả của tin nhắn (origin
) và kết nối một tập lệnh trình duyệt mới wschat_client.js
. Nó phân tích các tin nhắn (chúng ta sử dụng ký hiệu '#' để phân tách tác giả và văn bản) và điền vào các trường biểu mẫu với thông tin nhận được. Vì không có gì thay đổi từ quan điểm của giao thức WebSocket, chúng ta sẽ không cung cấp mã nguồn.
Trang web dịch vụ trò chuyện trên WebSocket
Bạn có thể khởi động nodejs với máy chủ trò chuyện wschat.js
và sau đó kết nối với nó từ nhiều tab trình duyệt. Mỗi kết nối nhận được một số duy nhất hiển thị trong tiêu đề. Văn bản từ trường Message
được gửi đến tất cả các máy khách khi nhấp vào Submit
. Sau đó, các biểu mẫu máy khách hiển thị cả tác giả của tin nhắn (nhãn ở dưới cùng bên trái) và chính văn bản (trường ở dưới cùng chính giữa).
Vậy là, chúng ta đã đảm bảo rằng máy chủ web với hỗ trợ web socket đã sẵn sàng. Hãy chuyển sang việc viết phần máy khách của giao thức trong MQL5.