Статьи

Чат в реальном времени с Readline & Socket.io от Node.js

Конечный продукт
Что вы будете создавать

Node.js имеет недооцененный модуль в своей стандартной библиотеке, что удивительно полезно. Модуль Readline делает то, что говорит на коробке: он читает строку ввода с терминала. Это можно использовать, чтобы задать пользователю один или два вопроса или создать подсказку внизу экрана. В этом уроке я намереваюсь продемонстрировать возможности Readline и создать чат CLI в реальном времени, поддерживаемый Socket.io. Клиент будет не только отправлять простые сообщения, но и иметь команды для эмоций с /me , личных сообщений с /msg и разрешать изменение псевдонимов с помощью /nick .

Это, вероятно, самое простое использование Readline:

1
2
3
4
5
6
7
8
var readline = require(‘readline’);
 
var rl = readline.createInterface(process.stdin, process.stdout);
 
rl.question(«What is your name? «, function(answer) {
    console.log(«Hello, » + answer );
    rl.close();
});

Мы включаем модуль, создаем интерфейс Readline со стандартными потоками ввода и вывода, затем задаем пользователю одноразовый вопрос. Это первое использование Readline: задавать вопросы. Если вам нужно что-то подтвердить с пользователем, возможно, в виде постоянно популярного «Вы хотите сделать это? Это.

Другая функция, предоставляемая Readline, — это приглашение, которое можно настроить по умолчанию на символ « > » и временно приостановить для предотвращения ввода. Для нашего чата Readline это будет наш основной интерфейс. Будет одно вхождение readline.question() чтобы попросить пользователя ввести псевдоним, но все остальное будет readline.prompt() .

Начнем с скучной части: зависимости. Этот проект будет использовать socket.io , пакет socket.io-client и ansi-color . Ваш файл packages.json должен выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
{
    «name»: «ReadlineChatExample»,
    «version»: «1.0.0»,
    «description»: «CLI chat with readline and socket.io»,
    «author»: «Matt Harzewski»,
    «dependencies»: {
        «socket.io»: «latest»,
        «socket.io-client»: «latest»,
        «ansi-color»: «latest»
    },
    «private»: true
}

Запустите npm install и все будет хорошо.

Для этого урока мы будем использовать невероятно простой сервер Socket.io. Это не становится более простым, чем это:

01
02
03
04
05
06
07
08
09
10
11
12
13
var socketio = require(‘socket.io’);
 
// Listen on port 3636
var io = socketio.listen(3636);
 
io.sockets.on(‘connection’, function (socket) {
 
    // Broadcast a user’s message to everyone else in the room
    socket.on(‘send’, function (data) {
        io.sockets.emit(‘message’, data);
    });
 
});

Все, что он делает, это принимает входящее сообщение от одного клиента и передает его всем остальным. Сервер, вероятно, будет более надежным для крупномасштабного приложения, но для этого простого примера этого должно быть достаточно.

Это должно быть сохранено в каталоге проекта как server.js .

Прежде чем перейти к интересной части, нам нужно включить наши зависимости, определить некоторые переменные и запустить интерфейс Readline и соединение с сокетом.

1
2
3
4
5
6
7
8
9
var readline = require(‘readline’),
socketio = require(‘socket.io-client’),
util = require(‘util’),
color = require(«ansi-color»).set;
 
 
var nick;
var socket = socketio.connect(‘localhost’, { port: 3636 });
var rl = readline.createInterface(process.stdin, process.stdout);

Код в значительной степени говорит сам за себя. У нас есть переменная псевдонима, соединение с сокетом (через пакет socket.io-client ) и интерфейс Readline.

В этом примере Socket.io будет подключаться к локальному хосту через порт 3636 , конечно, он будет изменен на домен и порт вашего собственного сервера, если вы создавали приложение для чата. (Нет смысла болтать с самим собой!)

Теперь для нашего первого использования Readline! Мы хотим попросить пользователя выбрать псевдоним, который будет идентифицировать их в чате. Для этого мы будем использовать метод Readline question() .

1
2
3
4
5
6
7
// Set the username
rl.question(«Please enter a nickname: «, function(name) {
    nick = name;
    var msg = nick + » has joined the chat»;
    socket.emit(‘send’, { type: ‘notice’, message: msg });
    rl.prompt(true);
});

Мы устанавливаем переменную nick ранее, в значение, полученное от пользователя, отправляем на сервер сообщение (которое будет передано другим клиентам), что наш пользователь присоединился к чату, затем переключаем интерфейс Readline обратно в режим приглашения. true значение, передаваемое в prompt() обеспечивает правильное отображение символа подсказки. (В противном случае курсор может переместиться в нулевую позицию на строке, и символ « > » не будет отображаться.)

К сожалению, в Readline есть неприятная проблема с методом prompt() . Это не очень хорошо работает с console.log() , который выводит текст в ту же строку, что и символ приглашения, оставляя беспорядочные символы « > » повсюду и другие странности. Чтобы исправить это, мы не будем использовать console.log где-либо в этом приложении, за исключением одного места. Вместо этого вывод должен быть передан этой функции:

1
2
3
4
5
6
function console_out(msg) {
    process.stdout.clearLine();
    process.stdout.cursorTo(0);
    console.log(msg);
    rl.prompt(true);
}

Это немного хакерское решение гарантирует, что текущая строка в консоли пуста, и что курсор находится в нулевой позиции перед выводом на печать. Затем он явно требует, чтобы подсказка снова выводилась после этого.

Поэтому в оставшейся части этого урока вы увидите console_out() вместо console.log() .

Пользователь может ввести два типа ввода: чат и команды. Мы знаем, что командам предшествует косая черта, поэтому их легко различить.

В Readline есть несколько обработчиков событий, но самым важным из них, несомненно, является line . При обнаружении символа новой строки во входном потоке (с помощью клавиши возврата или ввода) происходит это событие. Поэтому нам нужно подключиться к line для нашего обработчика ввода.

01
02
03
04
05
06
07
08
09
10
11
12
rl.on(‘line’, function (line) {
    if (line[0] == «/» && line.length > 1) {
        var cmd = line.match(/[az]+\b/)[0];
        var arg = line.substr(cmd.length+2, line.length);
        chat_command(cmd, arg);
 
    } else {
        // send chat message
        socket.emit(‘send’, { type: ‘chat’, message: line, nick: nick });
        rl.prompt(true);
    }
});

Если первым символом строки ввода является косая черта, мы знаем, что это команда, которая потребует дополнительной обработки. В противном случае мы просто отправляем обычное сообщение в чат и сбрасываем подсказку. Обратите внимание на разницу между данными, передаваемыми через сокет, и сообщением о присоединении на предыдущем шаге. Он использует другой type , поэтому принимающий клиент знает, как форматировать сообщение, и мы также передаем переменную nick .

Имя команды ( cmd ) и следующий за ней текст ( arg ) изолируются небольшим количеством регулярных выражений и магии подстрок, а затем мы передаем их функции, которая обрабатывает команду.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function chat_command(cmd, arg) {
    switch (cmd) {
 
        case ‘nick’:
            var notice = nick + » changed their name to » + arg;
            nick = arg;
            socket.emit(‘send’, { type: ‘notice’, message: notice });
            break;
 
        case ‘msg’:
            var to = arg.match(/[az]+\b/)[0];
            var message = arg.substr(to.length, arg.length);
            socket.emit(‘send’, { type: ‘tell’, message: message, to: to, from: nick });
            break;
 
        case ‘me’:
            var emote = nick + » » + arg;
            socket.emit(‘send’, { type: ’emote’, message: emote });
            break;
 
        default:
            console_out(«That is not a valid command.»);
 
    }
}

Если пользователь вводит /nick gollum , переменная nick сбрасывается на gollum , где он мог быть до того, как smeagol и на сервер smeagol уведомление.

Если пользователь вводит /msg bilbo Where is the precious? то же самое регулярное выражение используется для разделения получателя и сообщения, затем объект с типом сообщения отправляется на сервер. Это будет отображаться немного иначе, чем обычное сообщение, и не должно быть видно другим пользователям. По общему признанию, наш слишком простой сервер будет слепо отправлять сообщение всем, но клиент будет игнорировать сообщения, которые не адресованы правильному псевдониму. Более надежный сервер может быть более дискретным.

Команда emote используется в виде /me is eating second breakfast . К псевдониму добавляется псевдоним, который должен быть знаком любому, кто использовал IRC или играл в многопользовательскую ролевую игру, а затем отправляется на сервер.

Теперь клиенту нужен способ получения сообщений. Все, что нам нужно сделать, это подключиться к событию message клиента Socket.io и соответствующим образом отформатировать данные для вывода.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
socket.on(‘message’, function (data) {
    var leader;
    if (data.type == ‘chat’ && data.nick != nick) {
        leader = color(«<«+data.nick+»> «, «green»);
        console_out(leader + data.message);
    }
    else if (data.type == «notice») {
        console_out(color(data.message, ‘cyan’));
    }
    else if (data.type == «tell» && data.to == nick) {
        leader = color(«[«+data.from+»->»+data.to+»]», «red»);
        console_out(leader + data.message);
    }
    else if (data.type == «emote») {
        console_out(color(data.message, «cyan»));
    }
});

Сообщения с типом chat , которые не были отправлены клиентом с использованием нашего псевдонима, отображаются с псевдонимом и текстом чата. Пользователь уже может видеть, что он ввел в Readline, поэтому нет смысла выводить его снова. Здесь я использую пакет ansi-color , чтобы немного раскрасить вывод. Это не является строго необходимым, но это облегчает наблюдение за чатом.

Сообщения с типом notice или emote печатаются как есть, хотя и окрашены в голубой цвет.

Если сообщение является сообщением, а псевдоним совпадает с текущим именем этого клиента, вывод принимает форму [Somebody->You] Hi! , Конечно, это не очень личное. Если вы хотите видеть сообщения каждого , все, что вам нужно сделать, это && data.to == nick часть && data.to == nick . В идеале сервер должен знать, на какой клиент отправлять сообщение, а не отправлять его тем клиентам, которые в этом не нуждаются. Но это добавляет ненужную сложность, которая выходит за рамки этого урока.

Теперь посмотрим, все ли работает. Чтобы проверить это, запустите сервер, запустив node server.js а затем откройте пару новых окон терминала. В новых окнах запустите node client.js и введите псевдоним. После этого вы сможете общаться между ними, предполагая, что все идет хорошо.

Надеюсь, этот урок показал вам, как легко начать работу с модулем Readline. Вы можете попробовать добавить больше функциональности в приложение чата, для большей практики. И, наконец, ознакомьтесь с документацией Readline для полного API.