ts-phantom-client
TypeScript icon, indicating that this package has built-in type declarations

1.0.7 • Public • Published

ts-phantom

Сетевой движок с реализацией пакетов и обязательного шифрования, пакеты передаются моментально

Как это работает

Пара слов о том, как это работает. Почему на сервере практически не требуется никакого кода? Все дело в архитектуре движка и протокола в целом. Это прежде всего P2P-протокол (peer-to-peer), по этому сервер выполняет лишь роль связиста и ретранслятора данных которые отправляет ему пользователь, все эти данные зашифрованны, сервер их не хранит (смотреть исходный код) и не может прочитать (кроме системных пакетов).

У каждого пользователя при подключении к серверу - появляется свой ConnectionID (буквенно-цифровой ID из 6 латинских букв в верхнем регистре и чисел с нуля до девяти [A-Z0-9]), этот ConnectionID пользователь A любым путем связи (хоть почтовым голубем) сообщает пользователю B, после этого пользователь B инициализирует соединение с помощью специальной функции которая описана в этом документе, далее, после того, как соединение инициализировано, необходимо чтобы другая сторона, в данном случае пользователь А, принял запрос на соединение. ТОЛЬКО после того, как запрос на соединение принят другой стороной - создается КАНАЛ, по которому пользователи общаются. После того, как соединение создано - пользователям необходимо сделать одинаковый ключ шифрования (опять же, пользователь A может сообщить где угодно ключ шифрования пользователю B, возможно даже в завуалированном формате, например: слушай, а скажи свой пароль от вк? - XXX09A19D91DADERI. Когда кто-то будет пытаться расшифровать Ваши сообщения, например провайдер трафика, он даже не поймет как они зашифрованы)

Все данные по протоколу передаются в двоичном(бинарном) формате, но Вам об это даже не обязательно знать - протокол сам сериализует и десериализует данные по зарегистрированным в нем пакетам.

Установка

Флаг -d нужен чтобы установить движок в качестве зависимости

npm i -d ts-phantom

Сервер

Сервер на Phantom создать очень просто, это займет у Вас буквально две строчки кода

import { Phantom } from './protocol/Phantom';
import { ProtocolMode } from './protocol/sock/ProtocolMode';

/**
 * Первый аргумент - режим работы протокола, их всего два CLIENT и SERVER, 
 * нам нужен режим сервера, выбираем ProtocolMode.SERVER
 * Второй аргумент - порт, на котором будет запущен сервер
 */
let server : Phantom = new Phantom(ProtocolMode.SERVER, 7777);
/**
 * Включаем сервер, прослушиваем соединение. Все. На сервере больше ничего не нужно
 */
server.connect();

Теперь сервер уже готов принимать и обрабатывать Ваших клиентов.

Клиент

В первую очередь - нам необходимо установить соединение с сервером, делается это очень просто

import { ConnectionID } from './protocol/connection/ConnectionID';
import { Phantom } from './protocol/Phantom';
import { ProtocolMode } from './protocol/sock/ProtocolMode';

/**
 * Первый аргумент - режим протокола, так как в данном случае мы - это клиент, а не сервер
 * выбираем режим CLIENT
 * Второй аргумент - номер порта для соединения(если сервер работает на порте 7777 значит порт 7777)
 * Третий аргумент - адрес до серверной машины по протоколу ws(s), БЕЗ ПОРТА. То есть: если сервер у Вас
 * запущен локально, то адрес будет ws://127.0.0.1. Если ип вашего сервера 91.11.11.22, то адрес будет соответственно ws://91.11.11.22
 */
let client : Phantom = new Phantom(ProtocolMode.CLIENT, 7777, "ws://127.0.0.1");

/**
 * Как и в случае с сервером - необходимо вызвать функцию connect чтобы подключиться к серверу
 * первый и единственный аргумент в которой - это callback функция, которая вызывается, когда соединение с сервером успешно установлено. Она передает нам наш ConnectionID
 */
client.connect(function(id : ConnectionID){
    console.info(`Ура! Мы подключились к серверу. Наш ID ${id}`);
});

Запустите сервер, потом запустите клиент, клиентом будет выведено в консоль (вместо AF83X1 будут случаные 6 цифр)

Ура! Мы подключились к серверу. Наш ID AF83X1

Отлично! Соединение с сервером установлено! Теперь можно соединиться с другим клиентом(который тоже соединен с этим сервером).

client.connect(function(id : ConnectionID){
    console.info(`Ура! Мы подключились к серверу. Наш ID ${id}`);
    /**
     * С помощью вызова функции connect мы можем подключиться к другому пользователю
     * ВАЖНО!! Функцию соединения можно вызывать только после того как получен уже наш ID, то есть желательно в теле этого callbackа
     */
    client.getUserConnector().сonnect("ID_ПОЛЬЗОВАТЕЛЯ");
});

/**
 * Это подписка на событие. Всего событий 4, одно из них - ON_OPPONENT_CONNECT - вызывается, когда сторона, к которой мы применяли функцию client.getUserConnector().сonnect() - приняла наш запрос на соединение. Ровно после этого события можно начать обмен любой информацией с принявшей наше соединение стороной 
 */
client.eventBus.on(Event.ON_OPPONENT_CONNECT, function(id : ConnectionID) {
    console.info(`Соединение с ${id} успешно установлено, можно начинать общаться!`);
});

То есть, Вы подключаетесь к серверу - сервер выдает вам ConnectionID, Ваш друг тоже подключается к ЭТОМУ ЖЕ(это важно!) серверу, ему тоже выдается ConnectionID, он сообщает его Вам, и Вы с помощю функции описанной выше - соединяетесь с ним. Окей! Но что делать если с клиентом(с нами) хотят соединиться - просто подписываемся на это событие!

/**
 * Это событие ON_CONNECT_PROCESS - тут описываются весь процесс соединения. В каллбеке первый аргумент - ид, к которому относится событие, второй аргумент - описание события.
 */
client.eventBus.on(Event.ON_CONNECT_PROCESS, function(id : ConnectionID, response : ConnectResponse){
    if(response === ConnectResponse.CONNECT_THROW){
        /**
         * С нами попытался кто-то соединиться, мы можем принять или отклонить соединение 
         */
        console.info(`С нами хочет соединиться ${id}`);
        /**
         * Принимаем соединение
         */
        client.getUserConnector().accept(); 

        /**
         * Или можем его отклонить client.getUserConnector().cancel();
         */
    }
});

После того как мы приняли соединение - мы можем начать общаться. Итого на данный момент у нас получился такой код клиента

import { ConnectionID } from './protocol/connection/ConnectionID';
import { Event } from './protocol/eventbus/Event';
import { ConnectResponse } from './protocol/packet/handshake/PacketN2ConnectResponse';
import { Phantom } from './protocol/Phantom';
import { ProtocolMode } from './protocol/sock/ProtocolMode';


let client : Phantom = new Phantom(ProtocolMode.CLIENT, 7777, "ws://127.0.0.1");

client.connect(function(id : ConnectionID){
    /**
     * С сервером соединились, ID получили
     */
    console.info(`Ура! Мы подключились к серверу. Наш ID ${id}`);
    client.getUserConnector().сonnect("ID_ПОЛЬЗОВАТЕЛЯ");
});

/**
 * Отслеживаем весь процесс соединений входящих/исходящих
 */
client.eventBus.on(Event.ON_CONNECT_PROCESS, function(id : ConnectionID, response : ConnectResponse){
    if(response === ConnectResponse.CONNECT_THROW){
        /**
         * С нами попытался кто-то соединиться, мы можем принять или отклонить соединение 
         */
        console.info(`С нами хочет соединиться ${id}`);
        /**
         * Принимаем соединение
         */
        client.getUserConnector().accept(); 
        return;
    }
    if(response === ConnectResponse.DISCARDED){
        /**
         * Когда сторона, с которой мы пытались соединиться - отклоняет наш запрос (функция cancel()) - мы будем уведомлены об этом
         */
        console.info(`${id} отклонил Ваш запрос на соединение!`);
    }
    if(response === ConnectResponse.REQUESTED){
        /**
         * Соединение успешно запрошено (ожидается подтверждение от другой стороны)
         */
        console.info(`Соединение с ${id} успешно запрошено, ждем подтверждение`);
    }
});

/**
 * Когда кто-то принимает наше соединение, либо мы принимаем чье-то соединение - образуется КАНАЛ.
 * Когда обраузется канал с каким-то пользователем, выполняется это событие
 */
client.eventBus.on(Event.ON_OPPONENT_CONNECT, function(id : ConnectionID){
    console.info(`Ура! Мы подключились к пользователю ${id}, можно начинать общаться!`);
});

/**
 * Когда оппонент, с которым у нас был образован канал - отключается, то выполняется это событие
 */
client.eventBus.on(Event.ON_OPPONENT_DISCONNECT, function(id : ConnectionID){
    console.info(`Упс! ${id} отключился от сервера, соединение разорвано`)
});

С соединениями все понятно, теперь - отправка данных. Вот пользователь соединился, а дальше то что? А дальше они могут отправлять друг другу произвольные пакеты, и это уже следующий раздел - ОТПРАВКА ДАННЫХ

Отправка данных

Пакет(Packet) - минимальная единица данных, которая передается движком. Чтобы отправить какие-то данные по сети - нам нужно создать свой пакет.

Структура пакетов

Все пакеты должны быть унаследованы от родительского класса Packet. Каждый пакет имеет структуру {ID}{DATA}, где ID - ID пакета, всегда положительное число (отрицательные ID пакетов - системные), и DATA - любые данные пакета. Но первым делом ВСЕГДА идет ID, это протокол. Примеры пакетов, они имеют отрицательный ID, т.к они системные, в нашем же случае мы будем использовать всегда положительный ID.

Создадим свой первый пакет

Итак, создадим пакет, который будет передавать любое текстовое сообщение между двумя пользователями

import { BitStream, StringByteEncode } from "ts-bitstream";
import { Packet } from "../protocol/packet/Packet";

/**
 * Все пакеты должны быть наследованы(extends) от класса Packet
 * Я всегда добавляю цифру ID пакета в его название, это дело вкуса
 */
export class Packet1Message extends Packet {

    private message : string;

    /**
     * Конструктор пакета. ВСЕГДА ВСЕ АРГУМЕНТЫ должны быть НЕ ОБЯЗАТЕЛЬНЫМИ. Так как пакет могут не
     * только отправлять, но еще и получать. В таком случае аргументы будут недоступны, так как они на тот
     * момент будут еще закодированы. А вот при отправке пакета этот конструктор нам понадобится
     */
    constructor(message? : string){
        super();
        this.message = message;
    }

    /**
     * Обязательная фукнция, унаследованная от Packet. Она должна вернуть ID пакета. ID пакета - любое число
     */
    public getPacketId(): number {
        return 1;
    }

    /**
     * Функция recive срабатывает, когда пакет приходит клиенту 
     */
    public recive(bs: BitStream): Packet {
        /**
         * Читаем наше сообщение, вот по этому аргументы в конструкторе должны быть
         * не обязательными. Потому что поле заполняется только к моменту чтения пакета
         */
        this.message = bs.readString();
        return this;
    }

    /**
     * Эта функция вызывается движом когда пакет отправляется
     */
    public send(): BitStream {
        let bs : BitStream = new BitStream(); //создаем новый битовый поток для пакета
        bs.writeInt(this.getPacketId()); //как и было описано выше - первым делом записываем в пакет его ID, он всегда должен идти первый!
        bs.writeString(this.message, StringByteEncode.AUTOMATIC); //записываем сообщение
        return bs; //возвращаем готовый битовый поток
    }
    
    /**
     * Функция для получения сообщения, которое мы прочли
     */
    public getMessage() : string {
        return this.message;
    }
}

В коде встречаются функции связанные с битовыми потоками (writeInt, writeString, readInt, readString), это функции из библиотеки ts-bitstream, почитайте ее README если эти функции Вам непонятны. Если коротко: все, что они делают это читают определенный тип информации. Битовые потоки используется здесь потому что они очень компактные. Итак, мы создали наш первый пакет.

Регистрация пакета

Пакет мы создали, теперь нужно его зарегистрировать, при этом РЕГИСТРИРОВАТЬ НА СЕРВЕРЕ ЕГО НЕ НУЖНО, достаточно зарегистрировать только на клиенте, делается это вот так

let client : Phantom = new Phantom(ProtocolMode.CLIENT, 7777, "ws://127.0.0.1");

/**
 * Регистрация пакета. Можно регистрировать его в любом месте, но лучше сразу после получения инстанса Phantom
 */
client.getPacketManager().addPacket(new Packet1Message, function (packet : Packet1Message){
    /**
     * В переменной packet - у нас уже прочитанный и расшифрованный пакет. Вы можете просто обращаться к нему как к объекту
     */
    console.info(`Пришло сообщение: ${packet.getMessage()}`);
});

client.connect(function(id : ConnectionID){
...
Отправка пакетов

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

/**
 * Когда кто-то принимает наше соединение, либо мы принимаем чье-то соединение - образуется КАНАЛ.
 * Когда обраузется канал с каким-то пользователем, выполняется это событие
 */
client.eventBus.on(Event.ON_OPPONENT_CONNECT, function(id : ConnectionID){
    console.info(`Ура! Мы подключились к пользователю ${id}, можно начинать общаться!`);

    console.info(`Обмен ключами шифрования`);
    /**
     * На месте ABCDE12345 должен быть ключ шифрования, который используется у другой стороны. Другая сторона может получить свой ключ используя функцию client.getEncryptionKey() и сообщить вам. Но, допустим другая сторона назвала вам этот ключ - значит указываем его.
     */
    client.setEncryptionKey("ABCDE12345");
});

Как только обмен ключами шифрования произошел, можно начинать отправлять любые пакеты, давайте отправим пакет который мы только что создали

...
client.setEncryptionKey("ABCDE12345");
client.send(new Packet1Message("Приветик!!!"));
...

Запускаем сервер, запускаем двух клиентов. И как только они подключатся к друг другу - обоим отправится сообщение

Пришло сообщение: Приветик!!!

Почему обоим? Потому что событие Event.ON_OPPONENT_CONNECT сработает и у одного клиента, и у второго (так как между ними было установлено соединение), и так как пакет отправляется у нас именно в каллбеке этого события - они отправят его друг другу.

Весь код клиента

Это весь код клиента

//index.ts
import { ConnectionID } from './protocol/connection/ConnectionID';
import { Event } from './protocol/eventbus/Event';
import { ConnectResponse } from './protocol/packet/handshake/PacketN2ConnectResponse';
import { Phantom } from './protocol/Phantom';
import { ProtocolMode } from './protocol/sock/ProtocolMode';
import { Packet1Message } from './testpacket/Packet1Message';


let client : Phantom = new Phantom(ProtocolMode.CLIENT, 7777, "ws://127.0.0.1");

/**
 * Регистрация пакета. Можно регистрировать его в любом месте, но лучше сразу после получения инстанса Phantom
 */
client.getPacketManager().addPacket(new Packet1Message, function (packet : Packet1Message){
    /**
     * В переменной packet - у нас уже прочитанный и расшифрованный пакет. Вы можете просто обращаться к нему как к объекту
     */
    console.info(`Пришло сообщение: ${packet.getMessage()}`);
});

client.connect(function(id : ConnectionID){
    /**
     * С сервером соединились, ID получили
     */
    console.info(`Ура! Мы подключились к серверу. Наш ID ${id}`);
    client.getUserConnector().сonnect("ID_ПОЛЬЗОВАТЕЛЯ");
});

/**
 * Отслеживаем весь процесс соединений входящих/исходящих
 */
client.eventBus.on(Event.ON_CONNECT_PROCESS, function(id : ConnectionID, response : ConnectResponse){
    if(response === ConnectResponse.CONNECT_THROW){
        /**
         * С нами попытался кто-то соединиться, мы можем принять или отклонить соединение 
         */
        console.info(`С нами хочет соединиться ${id}`);
        /**
         * Принимаем соединение
         */
        client.getUserConnector().accept(); 
        return;
    }
    if(response === ConnectResponse.DISCARDED){
        /**
         * Когда сторона, с которой мы пытались соединиться - отклоняет наш запрос (функция cancel()) - мы будем уведомлены об этом
         */
        console.info(`${id} отклонил Ваш запрос на соединение!`);
    }
    if(response === ConnectResponse.REQUESTED){
        /**
         * Соединение успешно запрошено (ожидается подтверждение от другой стороны)
         */
        console.info(`Соединение с ${id} успешно запрошено, ждем подтверждение`);
    }
});

/**
 * Когда кто-то принимает наше соединение, либо мы принимаем чье-то соединение - образуется КАНАЛ.
 * Когда обраузется канал с каким-то пользователем, выполняется это событие
 */
client.eventBus.on(Event.ON_OPPONENT_CONNECT, function(id : ConnectionID){
    console.info(`Ура! Мы подключились к пользователю ${id}, можно начинать общаться!`);

    console.info(`Обмен ключами шифрования`);
    /**
     * На месте ABCDE12345 должен быть ключ шифрования, который используется у другой стороны. Другая сторона может получить свой ключ используя функцию client.getEncryptionKey() и сообщить вам. Но, допустим другая сторона назвала вам этот ключ - значит указываем его.
     */
    client.setEncryptionKey("ABCDE12345");
    client.send(new Packet1Message("Приветик!!!"));
});

/**
 * Когда оппонент, с которым у нас был образован канал - отключается, то выполняется это событие
 */
client.eventBus.on(Event.ON_OPPONENT_DISCONNECT, function(id : ConnectionID){
    console.info(`Упс! ${id} отключился от сервера, соединение разорвано`)
});
//testpacket/Packet1Message.ts
import { BitStream, StringByteEncode } from "ts-bitstream";
import { Packet } from "../protocol/packet/Packet";

/**
 * Все пакеты должны быть наследованы(extends) от класса Packet
 * Я всегда добавляю цифру ID пакета в его название, это дело вкуса
 */
export class Packet1Message extends Packet {

    private message : string;

    /**
     * Конструктор пакета. ВСЕГДА ВСЕ АРГУМЕНТЫ должны быть НЕ ОБЯЗАТЕЛЬНЫМИ. Так как пакет могут не
     * только отправлять, но еще и получать. В таком случае аргументы будут недоступны, так как они на тот
     * момент будут еще закодированы. А вот при отправке пакета этот конструктор нам понадобится
     */
    constructor(message? : string){
        super();
        this.message = message;
    }

    /**
     * Обязательная фукнция, унаследованная от Packet. Она должна вернуть ID пакета. ID пакета - любое число
     */
    public getPacketId(): number {
        return 1;
    }

    /**
     * Функция recive срабатывает, когда пакет приходит клиенту 
     */
    public recive(bs: BitStream): Packet {
        /**
         * Читаем наше сообщение, вот по этому аргументы в конструкторе должны быть
         * не обязательными. Потому что поле заполняется только к моменту чтения пакета
         */
        this.message = bs.readString();
        return this;
    }

    /**
     * Эта функция вызывается движом когда пакет отправляется
     */
    public send(): BitStream {
        let bs : BitStream = new BitStream(); //создаем новый битовый поток для пакета
        bs.writeInt(this.getPacketId()); //как и было описано выше - первым делом записываем в пакет его ID, он всегда должен идти первый!
        bs.writeString(this.message, StringByteEncode.AUTOMATIC); //записываем сообщение
        return bs; //возвращаем готовый битовый поток
    }
    
    /**
     * Функция для получения сообщения, которое мы прочли
     */
    public getMessage() : string {
        return this.message;
    }
}

Интересный момент

В теле пакетов, в функции отправки, мы записываем первым делом ID, потом остальные данные.

public send(): BitStream {
    let bs : BitStream = new BitStream(); //создаем новый битовый поток для пакета
    bs.writeInt(this.getPacketId()); //как и было описано выше - первым делом записываем в пакет его ID, он всегда должен идти первый!
    bs.writeString(this.message, StringByteEncode.AUTOMATIC); //записываем сообщение
    return bs; //возвращаем готовый битовый поток
}

А вот при чтении - мы сразу читаем данные. Почему так? Ведь мы записывали ID и данные, а читаем только данные

/**
 * Функция recive срабатывает, когда пакет приходит клиенту 
 */
public recive(bs: BitStream): Packet {
    //ОПА!!! НЕ ЧИТАЕМ ИД!!!
    this.message = bs.readString(); //читаем данные
    return this;
}

Все дело в том, что внутри класса BitStream есть указатель чтения (readPointer), который смещается определенным образом каждый раз когда мы что-то читаем, и так как в ядре движка ID пакета уже прочтен, то указатель чтения уже сместился на нужную нам позицию, и мы сразу можем читать данные

Package Sidebar

Install

npm i ts-phantom-client

Weekly Downloads

1

Version

1.0.7

License

ISC

Unpacked Size

153 kB

Total Files

87

Last publish

Collaborators

  • hellscoder