Если вы хотите создать простой веб-API, вам необходимо создать веб-сервер на данном этапе. В ASP.NET для создания сервера вам потребуется IIS. В PHP для этого вам потребуется использовать Apache/Nginx. Для начинающих , вам нужно увидеть так много шагов, прежде чем начать. , вам, возможно, придется сдаться, но так просто открыть веб-сервер в Node.js, мы можем использовать Net, Dgram, HTTP, HTTPS и другие модули для достижения с помощью нескольких строк простого кода.
об авторе: Джун Май, разработчик Nodejs, сертифицированный автор МООК, молодежь после 90-х, которая любит технологии и любит делиться, добро пожаловать на внимание.Стек технологий Nodejsи проект с открытым исходным кодом Githubwww.nodejs.red
Быстрая навигация
- сетевая модель
- Знакомство с протоколом TCP
- Net модуль для создания службы TCP
- Проблема с липким пакетом TCP
Руководство по собеседованию
- Что такое TCP-протокол? При каких обстоятельствах вы бы выбрали протокол TCP? Справочный текст Интервью1
- Что такое липкие пакеты TCP? Как это решить? Справочный текст Интервью2
сетевая модель
Большинство учащихся знакомы с HTTP и HTTPS, которые обычно используются для взаимодействия браузер-сервер или сервер-сервер. Два других, Net и Dgram, могут быть относительно незнакомы. Эти два являются транспортными уровнями, основанными на сетевых моделях. На следующем рисунке показано взаимосвязь между семиуровневой моделью OSI и пятиуровневой моделью TCP/IP. Пунктирная линия используется для обозначения транспортного уровня. Для верхнего прикладного уровня (HTTP/HTTPS и т. д.) также реализованы на основе Протокол TCP этого уровня, поэтому, если вы хотите использовать Node.js для разработки на стороне сервера, вы также должны освоить модуль Net, которому также посвящена эта статья.
Знакомство с протоколом TCP
Интервью1: Некоторые понятия еще нужно прояснить, что такое протокол TCP? При каких обстоятельствах вы бы выбрали протокол TCP?
TCP — это протокол управления передачей, и мы будем использовать этот протокол в большинстве случаев, потому что это более надежный протокол передачи данных со следующими тремя характеристиками:
- ориентированный на ссылки: Другой хост должен быть в сети и установить связь.
- байтовый поток: Вы даете мне кучу данных потока байтов, и я отправлю их вам, но последнее слово в том, сколько отправлять каждый раз, остается за мной.Каждый раз, когда я выбираю байт для отправки, я привожу порядковый номер.Это порядковый номер для отправки Номер байта с наименьшим номером в этом сегменте байтов.
- надежный: чтобы гарантировать, что данные поступают на хост другой стороны упорядоченным образом, каждый раз, когда часть данных отправляется, ожидается получение ответа другой стороны.Если ответ другой стороны получен в течение указанного времени, он подтверждается что данные прибыли.Если ответ другой стороны не получен через определенное время, это Думает, что другая сторона не получила его, он повторно отправляет его.
Вышеупомянутые три функции говорят о том, что TCP является ориентированным на канал и надежным.Одной из его примечательных особенностей является то, что перед передачей будет 3-стороннее рукопожатие.Процесс реализации выглядит следующим образом:
В процессе трехстороннего рукопожатия TCP клиент и сервер соответственно предоставляют сокет для установления соединения. После этого клиент и сервер отправляют данные друг другу по этой ссылке.
Net модуль для создания службы TCP
После понимания некоторых концепций TCP, приведенных выше, мы начинаем создавать сервер TCP и экземпляр клиента. Здесь нам нужно использовать модуль Net Node.js, который предоставляет некоторые интерфейсы для базовой связи. Этот модуль можно использовать для создания сети на основе на потоковом сервере TCP или IPC (net.createServer()) и клиенте (net.createConnection()).
Создать службу TCP
Вы можете использовать new net.Server для создания ссылки на TCP-сервер или использовать фабричную функцию net.createServer(), внутренняя реализация createServer() также внутренне вызывает конструктор сервера для создания TCP-объекта и new net. Сервер тот же, код выглядит так:
function createServer(options, connectionListener) {
return new Server(options, connectionListener);
}
function Server(options, connectionListener) {
if (!(this instanceof Server))
return new Server(options, connectionListener);
// Server 类内部还是继承了 EventEmitter,这个不在本节范围
EventEmitter.call(this);
...
События службы TCP
Прежде чем запускать код, сначала разберитесь с связанными с ним событиями, обратитесь к официальному сайту.узел будет .capable/api/net.htm…, я не буду представлять их здесь все. Вот некоторые из наиболее часто используемых, и они будут объяснены с помощью примеров кода. Исходя из этого, вы можете обратиться к официальному сайту, чтобы попрактиковаться в некоторых других событиях или методах.
События TCP-сервера
- listen: , который является server.listen();
- соединение: срабатывает при установлении нового соединения, то есть каждый раз, когда принимается обратный вызов клиента, параметр socket является экземпляром net.createServer, который также может быть записан в net.createServer(function(socket) {}) метод
- close: срабатывает, когда сервер закрывается (server.close()). Если есть подключения, это событие не будет запущено, пока все подключения не будут завершены.
- ошибка: ошибки перехвата, такие как прослушивание существующего порта, сообщит об ошибке: ошибка прослушивания EADDRINUSE
Методы события TCP-соединения
- данные: когда один конец вызывает метод write() для отправки данных, другой конец получает их через событие socket.on('data'), которое можно понимать как чтение данных.
- end: Каждое соединение сокета появится один раз.Например, после того, как клиент отправит сообщение и выполнит терминал Ctrl + C, он получит
- ошибка: прослушайте сообщение об ошибке сокета
- write: write — это метод (socket.write()). Вышеприведенное событие данных предназначено для чтения данных. Метод записи здесь для записи данных на другой конец.
Реализация кода сервера TCP
const net = require('net');
const HOST = '127.0.0.1';
const PORT = 3000;
// 创建一个 TCP 服务实例
const server = net.createServer();
// 监听端口
server.listen(PORT, HOST);
server.on('listening', () => {
console.log(`服务已开启在 ${HOST}:${PORT}`);
});
server.on('connection', socket => {
// data 事件就是读取数据
socket.on('data', buffer => {
const msg = buffer.toString();
console.log(msg);
// write 方法写入数据,发回给客户端
socket.write(Buffer.from('你好 ' + msg));
});
})
server.on('close', () => {
console.log('Server Close!');
});
server.on('error', err => {
if (err.code === 'EADDRINUSE') {
console.log('地址正被使用,重试中...');
setTimeout(() => {
server.close();
server.listen(PORT, HOST);
}, 1000);
} else {
console.error('服务器异常:', err);
}
});
Реализация клиентского кода TCP
const net = require('net');
const client = net.createConnection({
host: '127.0.0.1',
port: 3000
});
client.on('connect', () => {
// 向服务器发送数据
client.write('Nodejs 技术栈');
setTimeout(() => {
client.write('JavaScript ');
client.write('TypeScript ');
client.write('Python ');
client.write('Java ');
client.write('C ');
client.write('PHP ');
client.write('ASP.NET ');
}, 1000);
})
client.on('data', buffer => {
console.log(buffer.toString());
});
// 例如监听一个未开启的端口就会报 ECONNREFUSED 错误
client.on('error', err => {
console.error('服务器异常:', err);
});
client.on('close', err => {
console.log('客户端链接断开!', err);
});
Адрес реализации исходного кода
https://github.com/Q-Angelo/project-training/tree/master/nodejs/net/chapter-1-client-server
Демонстрационный тест клиента и сервера
Сначала запустите сервер, затем запустите клиент, клиент вызывает три раза, и результат печати выглядит следующим образом:
Сервер
$ node server.js
服务已开启在 127.0.0.1:3000
# 第一次
Nodejs 技术栈
JavaScript
TypeScript Python Java C PHP ASP.NET
# 第二次
Nodejs 技术栈
JavaScript TypeScript Python Java C PHP ASP.NET
клиент
$ node client.js
# 第一次
你好 Nodejs 技术栈
你好 JavaScript
你好 TypeScript Python Java C PHP ASP.NET
# 第二次
你好 Nodejs 技术栈
你好 JavaScript TypeScript Python Java C PHP ASP.NET
На стороне клиента я использую client.write() для отправки данных несколько раз, но нормальными являются только те, которые отличаются от setTimeout.Похоже, что непрерывная отправка в setTimeout не возвращается каждый раз, но будет случайным образом комбинироваться и возвращаться, Зачем? Давайте посмотрим на введение проблемы с липкими пакетами TCP ниже.
Проблема с липким пакетом TCP
Интервью 2: Что такое липкий пакет TCP? Как это решить?
Приведенный выше пример, наконец, поднимает вопрос, почему клиент постоянно отправляет данные на сервер и получает объединенный ответ? Это также распространенная проблема с липкими пакетами в TCP.Клиент (отправляющая сторона) будет буферизовать несколько блоков данных, отправленных вместе, за короткий период времени (буфер отправителя) перед отправкой, чтобы сформировать большой блок данных и отправить его вместе., приемник также имеетбуфер приемника,Полученные данные сначала сохраняются в буфере приемника, а затем программа считывает отсюда часть данных для потребления, это также позволяет сократить потребление операций ввода-вывода для достижения оптимизации производительности.
Проблемное мышление: когда данные поступают в буфер, чтобы начать отправку?
это зависит отКонтроль перегрузки TCP, является одним из управляющих факторов при определении количества байтов, которые могут быть отправлены в любое время, и является средством предотвращения перегрузки канала между отправителем и получателем, см. Википедию:en.wikipedia.org/wiki/Контроль перегрузки TCP…
Решение для липких пакетов TCP?
- Вариант 1: отложить отправку
- Вариант 2: отключить алгоритм Нэгла
- Вариант 3: Упаковка/Распаковка
Вариант 1: отложить отправку
Одним из самых простых решений является отсрочка отправки и сон на некоторое время, однако, хотя это решение простое, его недостатки также очевидны, а эффективность передачи сильно снижается, что явно не подходит для сценариев с частыми взаимодействиями. Первая модификация выглядит следующим образом:
client.on('connect', () => {
client.setNoDelay(true);
// 向服务器发送数据
client.write('Nodejs 技术栈');
const arr = [
'JavaScript ',
'TypeScript ',
'Python ',
'Java ',
'C ',
'PHP ',
'ASP.NET '
]
for (let i=0; i<arr.length; i++) {
(function(val, k){
setTimeout(() => {
client.write(val);
}, 1000 * (k+1))
}(arr[i], i));
}
})
Консоль выполняет команду node client.js, и вроде бы все ок, липкого пакета нет, но такая ситуация используется только в сценариях с низкой частотой взаимодействия.
$ node client.js
你好 Nodejs 技术栈
你好 JavaScript
你好 TypeScript
你好 Python
你好 Java
你好 C
你好 PHP
你好 ASP.NET
Адрес реализации исходного кода
https://github.com/Q-Angelo/project-training/tree/master/nodejs/net/chapter-2-delay
Вариант 2: алгоритм Нэгла
Алгоритм Нейгла — это алгоритм, повышающий эффективность передачи по сети и избегающий переполнения сети большим количеством небольших блоков данных.Он ожидает отправки как можно больших блоков данных, поэтому каждый раз, когда запрашивается блок данных, отправлено в TCP, отправка TCP не выполняется немедленно, а ожидает в течение короткого периода времени.
Когда сеть переполнена большим количеством небольших блоков данных, алгоритм Nagle может агрегировать небольшие блоки данных и отправлять их вместе, чтобы уменьшить перегрузку сети. Это по-прежнему очень полезно, но не требуется во всех сценариях. Например, REPL взаимодействие с терминалом, когда пользователь вводит один символ, чтобы получить ответ, поэтому в Node.js вы можете установить метод socket.setNoDelay() для отключения алгоритма Nagle.
const server = net.createServer();
server.on('connection', socket => {
socket.setNoDelay(true);
})
Отключение алгоритма Нейгла не всегда эффективно, т. к. он сливается на стороне сервера.Когда TCP получает данные, он сначала сохраняет их в собственном буфере, а затем уведомляет приложение о получении.Вынос данных из буфера TCP также приведет к тому, что несколько блоков данных будут сохранены в буфере TCP, которые будут формировать липкие пакеты.
Вариант 3: Упаковка/Распаковка
Первые два решения не особо идеальны, вот третьеУпаковка/распаковка, который также широко используется в отрасли.Здесь используется метод кодирования длины, а стороны связи согласовывают формат.Разделите сообщение на заголовок сообщения фиксированной длины (Header) и тело сообщения переменной длины (Body)., когда заголовок сообщения читается для получения длины, занимаемой содержимым, а количество байтов содержимого тела сообщения, прочитанного позже, равно количеству байтов байтового заголовка, мы считаем его полным пакетом .
Порядковый номер заголовка сообщения (Header) | Длина тела сообщения (заголовок) | Тело сообщения (тело) |
---|---|---|
SerialNumber | bodyLength | body |
2 (байт) | 2 (байт) | N (байт) |
Буфер априорных знаний
Следующее будет реализовано с помощью кодирования, но прежде чем вы начнете, я надеюсь, вы понимаете Buffer, вы можете обратиться к статье Buffer, которую я написал ранее.Что такое буфер в Node.js?, я перечислю буферы, которые необходимо использовать на этот раз, в качестве пояснения, что будет полезно для учащихся, не знакомых с буферами.
- Buffer.alloc(size[, fill[, encoding]]): Инициализировать буферное пространство размером с размер, заполненное 0 по умолчанию, или вы можете указать заполнение для пользовательского заполнения.
- buf.writeInt16BE(value[, offset]): значение — это значение буфера, которое будет записано, смещение — это место, где начинается запись смещения.
- buf.writeInt32BE(value[, offset]): Параметр аналогичен writeInt16BE, разница в том, что writeInt16BE означает запись 16-битного целого числа с высоким приоритетом, а writeInt32BE означает запись 32-битного целого числа с высоким приоритетом.
- buf.readInt16BE([offset]): читать 16-битное целое со старшим порядком вперед, смещение — это количество байтов, которые необходимо пропустить перед чтением
- buf.readInt32BE([offset]): читать 32-битное целое число со старшим порядком вперед, смещение — это количество байтов, которые нужно пропустить перед чтением
Реализация кодирования/декодирования
Основная передача TCP основана на двоичных данных, но наш прикладной уровень обычно представляет собой простые для выражения строки, числа и т. д. Здесь первым шагом в реализации кодирования является преобразование наших данных в двоичные данные через буфер. также требуется операция декодирования, и все в коде, который реализован следующим образом:
// transcoder.js
class Transcoder {
constructor () {
this.packageHeaderLen = 4; // 包头长度
this.serialNumber = 0; // 定义包序号
this.packageSerialNumberLen = 2; // 包序列号所占用的字节
}
/**
* 编码
* @param { Object } data Buffer 对象数据
* @param { Int } serialNumber 包序号,客户端编码时自动生成,服务端解码之后在编码时需要传入解码的包序列号
*/
encode(data, serialNumber) {
const body = Buffer.from(data);
const header = Buffer.alloc(this.packageHeaderLen);
header.writeInt16BE(serialNumber || this.serialNumber);
header.writeInt16BE(body.length, this.packageSerialNumberLen); // 跳过包序列号的前两位
if (serialNumber === undefined) {
this.serialNumber++;
}
return Buffer.concat([header, body]);
}
/**
* 解码
* @param { Object } buffer
*/
decode(buffer) {
const header = buffer.slice(0, this.packageHeaderLen); // 获取包头
const body = buffer.slice(this.packageHeaderLen); // 获取包尾部
return {
serialNumber: header.readInt16BE(),
bodyLength: header.readInt16BE(this.packageSerialNumberLen), // 因为编码阶段写入时跳过了前两位,解码同样也要跳过
body: body.toString(),
}
}
/**
* 获取包长度两种情况:
* 1. 如果当前 buffer 长度数据小于包头,肯定不是一个完整的数据包,因此直接返回 0 不做处理(可能数据还未接收完等等)
* 2. 否则返回这个完整的数据包长度
* @param {*} buffer
*/
getPackageLength(buffer) {
if (buffer.length < this.packageHeaderLen) {
return 0;
}
return this.packageHeaderLen + buffer.readInt16BE(this.packageSerialNumberLen);
}
}
module.exports = Transcoder;
Модернизация клиента
const net = require('net');
const Transcoder = require('./transcoder');
const transcoder = new Transcoder();
const client = net.createConnection({
host: '127.0.0.1',
port: 3000
});
let overageBuffer=null; // 上一次 Buffer 剩余数据
client.on('data', buffer => {
if (overageBuffer) {
buffer = Buffer.concat([overageBuffer, buffer]);
}
let packageLength = 0;
while (packageLength = transcoder.getPackageLength(buffer)) {
const package = buffer.slice(0, packageLength); // 取出整个数据包
buffer = buffer.slice(packageLength); // 删除已经取出的数据包,这里采用的方法是把缓冲区(buffer)已取出的包给截取掉
const result = transcoder.decode(package); // 解码
console.log(result);
}
overageBuffer=buffer; // 记录剩余不完整的包
}).on('error', err => { // 例如监听一个未开启的端口就会报 ECONNREFUSED 错误
console.error('服务器异常:', err);
}).on('close', err => {
console.log('客户端链接断开!', err);
});
client.write(transcoder.encode('0 Nodejs 技术栈'));
const arr = [
'1 JavaScript ',
'2 TypeScript ',
'3 Python ',
'4 Java ',
'5 C ',
'6 PHP ',
'7 ASP.NET '
]
setTimeout(function() {
for (let i=0; i<arr.length; i++) {
console.log(arr[i]);
client.write(transcoder.encode(arr[i]));
}
}, 1000);
Трансформация сервера
const net = require('net');
const Transcoder = require('./transcoder');
const transcoder = new Transcoder();
const HOST = '127.0.0.1';
const PORT = 3000;
let overageBuffer=null; // 上一次 Buffer 剩余数据
const server = net.createServer();
server.listen(PORT, HOST);
server.on('listening', () => {
console.log(`服务已开启在 ${HOST}:${PORT}`);
}).on('connection', socket => {
// data 事件就是读取数据
socket
.on('data', buffer => {
if (overageBuffer) {
buffer = Buffer.concat([overageBuffer, buffer]);
}
let packageLength = 0;
while (packageLength = transcoder.getPackageLength(buffer)) {
const package = buffer.slice(0, packageLength); // 取出整个数据包
buffer = buffer.slice(packageLength); // 删除已经取出的数据包,这里采用的方法是把缓冲区(buffer)已取出的包给截取掉
const result = transcoder.decode(package); // 解码
console.log(result);
socket.write(transcoder.encode(result.body, result.serialNumber));
}
overageBuffer=buffer; // 记录剩余不完整的包
})
.on('end', function(){
console.log('socket end')
})
.on('error',function(error){
console.log('socket error', error);
});
}).on('close', () => {
console.log('Server Close!');
}).on('error', err => {
if (err.code === 'EADDRINUSE') {
console.log('地址正被使用,重试中...');
setTimeout(() => {
server.close();
server.listen(PORT, HOST);
}, 1000);
} else {
console.error('服务器异常:', err);
}
});
запустить тест
Консоль выполняет узел server.js для запуска сервера, а затем выполняет узел client.js для запуска теста клиента.Вывод выглядит следующим образом:
$ node client.js
{ serialNumber: 0, bodyLength: 18, body: '0 Nodejs 技术栈' }
1 JavaScript
2 TypeScript
3 Python
4 Java
5 C
6 PHP
7 ASP.NET
{ serialNumber: 1, bodyLength: 13, body: '1 JavaScript ' }
{ serialNumber: 2, bodyLength: 13, body: '2 TypeScript ' }
{ serialNumber: 3, bodyLength: 9, body: '3 Python ' }
{ serialNumber: 4, bodyLength: 7, body: '4 Java ' }
{ serialNumber: 5, bodyLength: 4, body: '5 C ' }
{ serialNumber: 6, bodyLength: 6, body: '6 PHP ' }
{ serialNumber: 7, bodyLength: 10, body: '7 ASP.NET ' }
В приведенных выше результатах в функции setTimeout мы сначала отправляем несколько фрагментов данных одновременно, а затем возвращаем их один за другим.В то же время мы печатаем порядковый номер пакета, длину тела сообщения и тело сообщения пакета. определяется заголовком пакета, и они находятся во взаимном соответствии.Вышеупомянутая проблема с липким пакетом также была решена. Упаковка/распаковка немного сложны.Вышеприведенный код представил идею реализации максимально просто.Адрес кода реализации приведен ниже, который может быть использован в качестве справки или может быть реализован по-разному.
https://github.com/Q-Angelo/project-training/tree/master/nodejs/net/chapter-3-package