
UDP (протокол пользовательских дейтаграмм)
Как говорится в Википедии, UDP — это протокол, который выполняется по интернет-протоколу (IP), и ему не требуется предварительная передача сообщений для настройки любого канала связи или пути данных. Из-за механизма минимального протокола в нем нет диалогов установления связи и нет гарантии доставки, заказа или защиты от дублирования. Но у него есть контрольная сумма для целостности данных.
Передача сообщений UPD активно используется в распределенных системных приложениях. Но мы не можем использовать row UDP, потому что у него нет никакой гарантии доставки. Сегодня мы собираемся добавить некоторые механизмы надежности для передачи сообщений UPD на примере NodeJS.
Вы не можете отправлять JSON или любые другие данные через сообщение UPD. Но вы можете преобразовать объект JSON в строку и отправить его.
Прежде чем идти дальше, позвольте мне объяснить, что мы собираемся здесь делать. Во-первых, мы создаем сокет дейтаграммы, используя пакет с именем dgram, который поставляется с NodeJS (установка npm не требуется). Затем привяжите порт к нашему сокету и заставьте его слушать. Тем временем мы создадим еще один сервер (в данном случае на самом деле клиент. Мы используем его для отправки сообщений), такой же, как этот, который прослушивает другой порт и отправляет простое строковое сообщение на сервер, который мы создали ранее.
Поскольку наше приложение предназначено для создания протокола обмена данными с распределенной системой, у нас должно быть только одно приложение (тот же код). Поэтому в моем примере я объясню все, используя один файл, но работающий с разными номерами портов. Чтобы выполнить тот же код другим способом, я собираюсь использовать process.arvg массив для получения параметров. Посмотрим, как это будет работать.
const dgram = require('dgram');
const udpServer = dgram.createSocket('udp4');
udpServer.on('listening', () => {
console.log('UDP server is listing...');
});
const init = (port) => {
udpServer.bind(port);
udpServer.on('message', (msgStream, rinfo) => {
console.log('msg is', msgStream.toString());
console.log('rinfo', rinfo);
});
};
const send = (target) => {
const data = 'HELLO WORLD.. !';
udpServer.send(data, 0, data.length, target, '127.0.0.1', (err, len) => {
console.log('error', err);
console.log('sent length', len);
});
};
// when you run this file make sure these parameter to be passed
// myPort, <s, c>, target (if c)
init(parseInt(process.argv[2]));
if (process.argv[3] === 'c') {
console.log('send message');
send(parseInt(process.argv[4]));
}
Я предполагаю, что вы знаете о массиве process.argv. Он используется для управления сервером, как мы хотим. При выполнении файла порядок аргументов следующий: 1) указан номер порта. 2) является ли он сервером или клиентом с помощью «s» или «c». 3) номер целевого порта. См. пример выполнения ниже. Сохраните файл server.js, перейдите по пути в терминале и выполните следующие команды.
node server.js 3000 s # this will start one server
Теперь откройте другое окно терминала и выполните следующую команду
node server.js 4000 c 3000 # this will start the client and send a message
На самом деле здесь происходит то, что первая команда создает сервер с номером порта 3000, а вторая команда создает сервер с номером порта 4000. Затем вторая команда (назовем ее клиентом) отправляет сообщение на порт 3000. У нас есть 3 команды. строковые аргументы при выполнении команды, и если вы хотите получить более сложные данные, вы можете иметь такие пакеты, как minimis . Наконец, я дам вам полный исходный код с этими функциями. Но пока мы собираемся строить только коммуникации.
До сих пор мы отправляем текстовые сообщения. Давайте сделаем это более сложным, отправив объекты JSON. На самом деле мы используем то же самое, но JSON, который преобразуется в строку с помощью JSON.stringify(). Тогда мы можем легко управлять отправляемыми данными.
Теперь определим структуру пакетов данных, проходящих через сеть.
UPD_Packet : {
id -> unique id for each message
version -> version number
body -> message body(can be json or string)
type -> type of the udp packet (RES, ACK, REQ)
}
Атрибут id представляет собой случайно сгенерированный идентификатор для уникальной идентификации запроса и предоставления ответа. Если вы хотите, вы можете использовать пакеты uuid, чтобы получить случайный идентификатор для сообщений.
Кроме того, атрибут type представляет, что это за сообщение.
REQ : Request ( client to server)
ACK : Acknowledgement ( server to client / acknowledging the
request is received)
RES : Response ( server to client / response message for a
particular request)
Давайте обсудим, как этот протокол будет работать.
Сначала клиентский узел делает запрос ( REQ ) для серверного узла и ожидает ACK от сервера. В этом примере мы отправляем одно и то же сообщение запроса с увеличивающимся номером версии каждые «500 мс», пока его не получит ACK. Он проверит 5 раз, и в течение этого интервала времени подтверждение не будет получено, тогда будет выдана ошибка Timeout .
После отправки сообщения мы привязываем функцию обратного вызова и другую полезную информацию к вызову карты (обычный объект) responseHandlersMap с идентификатором, который мы создаем при отправке запроса в качестве ключа.
На сервере сообщение преобразуется в формат JSON и проверяется, какой тип сообщения получен.
Если это REQ , сервер отправляет клиенту ACK, чтобы указать, что сообщение успешно принято сервером. После этого мы создадим два объекта, которые несут данные о запросах и ответах. В объекте запроса есть свойства body и rinfo, содержащие тело сообщения и информацию об отправителе соответственно. В теле ответа есть только одна функция с именем json для завершения запроса путем передачи ответа клиенту. Внутри этой функции мы берем один аргумент, содержащий ответ сервера, и заключаем его в UPD_Packet то, что мы упоминали выше. Затем вызовите метод send, чтобы отправить строковый объект клиенту.
Если тип сообщения ACK , это может быть подтверждение запроса, который мы сделали перед этим с этого конкретного узла. Таким образом, наш responseHandlersMap должен иметь запись для этого запроса. Найдите его по его идентификатору и отметьте его как полученное подтверждение (resHandler.ack = true;). Поскольку мы уверены, что сервер получил запрос, мы больше не будем отправлять запросы. Для этого прекратите вызов метода setTimout(), очистив тайм-аут.
Если тип сообщения RES, найдите нужную запись из responseHandlersMap и выполните функцию обработчика с полученными данными. Если вы внимательно изучите код, то увидите, что вызов атрибута done имеет значение true. Это потому, что перестаньте вызывать один и тот же метод снова и снова для каждого ответа.
Хватит читать.... Просто посмотрите код.
const dgram = require('dgram');
const udpServer = dgram.createSocket('udp4');
udpServer.on('listening', () => {
console.log('UDP server is listing...');
});
const REQUEST_TYPES = {
ACK: 'ACK',
REQ: 'REQ',
RES: 'RES',
};
const responseHandlersMap = {};
init = (port, cb) => {
udpServer.bind(port);
udpServer.on('message', (msgStream, rinfo) => {
const udpStream = JSON.parse(msgStream.toString());
let resHandler = responseHandlersMap[udpStream.id];
// create req, res object to pass handler.
if (udpStream.type === REQUEST_TYPES.REQ) {
// create the acknowledgement
const ack = JSON.stringify({ type: REQUEST_TYPES.ACK, ok: 1, id: udpStream.id });
// send ACK
udpServer.send(ack, 0, ack.length, rinfo.port, rinfo.address);
const request = {
// create request object
body: udpStream.body,
rinfo: rinfo,
};
const response = {
// response object
json: (data) => {
const resString = JSON.stringify({
body: data,
id: udpStream.id,
type: REQUEST_TYPES.RES,
});
udpServer.send(resString, 0, resString.length, rinfo.port, rinfo.address);
},
};
// execute the call back with request and response object.
if (cb) cb(request, response);
} else if (udpStream.type === REQUEST_TYPES.ACK) {
if (resHandler) {
resHandler.ack = true;
resHandler.stopSending();
}
} else if (udpStream.type === REQUEST_TYPES.RES) {
if (resHandler && !resHandler.done) {
resHandler.stopSending();
// not call the response handler two time
resHandler.done = true;
resHandler.handler(null, udpStream, udpStream.body);
}
}
});
};
send = (target, data, cb) => {
// generate a unique random number
const msgId = Math.random().toString().substring(2);
let version = 0; // message version
const fn = () => {
version += 1; // increase message version
if (version > 5) {
// message limit
return cb({ error: 'TIMEOUT' });
}
// message Object
let msg_send = {
id: msgId,
version: version,
body: data,
type: REQUEST_TYPES.REQ,
};
msg_send = JSON.stringify(msg_send);
// send the message
udpServer.send(msg_send, 0, msg_send.length, target.port, target.ip, (a, b) => {
let timeoutRef;
// append a response handler object to the map
responseHandlersMap[msgId] = {
handler: cb,
ack: false,
done: false,
stopSending: () => {
clearTimeout(timeoutRef);
},
};
timeoutRef = setTimeout(() => {
return fn();
}, 500);
});
};
// put an interval to send the message until get an ack
fn();
};
// when you run this file make sure these parameter to be passed
// node server.js myPort <s / c> target (add target only if c)
const myPort = parseInt(process.argv[2]);
const type = process.argv[3];
const serverPort = parseInt(process.argv[4] || '0');
// initialize the server
init(myPort, (req, res) => {
// this is the request handling function.
const { number1, number2 } = req.body;
res.json({ sum: number1 + number2 });
});
// make a request from the client
if (type === 'c') {
send({ port: serverPort, ip: '127.0.0.1' }, { number1: 39, number2: 34 }, (err, upstreamData, response) => {
console.log(response);
});
}
Вы можете вставить этот сегмент кода в файл и выполнить его с помощью следующих команд.
Запустите сервер:
node server.js 3000 s # this will start one server
И откройте новое окно терминала и запустите клиент:
node server.js 4000 c 3000 # this will start the client and send a message
Вы должны увидеть ответ суммирования двух значений, переданных как number1 и number2 от клиента.
Спасибо и надеюсь, вам понравилось.