TCP и UDP сервера с использованием Netty 4

в 1:00, , рубрики: java, Анализ и проектирование систем, высокая производительность, Программирование, метки: , , , , , ,

Являясь Unity разработчиком, я со временем дошёл до того этапа, когда возникла необходимость написания сервера. Передо мной стояло много неизведанных троп сетевого программирования, в котором я потом повяз по голову. Прыгал между C++, C# и Java. После долгий скитаний я нашёл то, чему я сейчас говорю спасибо. Об этом я и хочу поведать.

Сначала был выбран путь такой: использовать стандартные сокеты передавая всё по TCP протоколу. Кто-то может подумать, что это в принципе нормально, но как бы не так.

По своей неопытности я столкнулся с тем, что сообщения приходили от клиента потоком и что бы я не делал — целостность нарушалась. Как всегда — если не получается, то читаем инструкцию. В итоге я перешёл на UDP протокол и избавился почти от всех проблем, которые у меня были.

Отличия TCP от UDP:

  • TCP гарантирует доставку пакетов данных в неизменных виде, последовательности и без потерь, UDP ничего не гарантирует.
  • TCP требует заранее установленного соединения, UDP соединения не требует.
  • UDP обеспечивает более высокую скорость передачи данных.
  • TCP надежнее и осуществляет контроль над процессом обмена данными.
  • UDP предпочтительнее для программ, воспроизводящих потоковое видео и сетевых игр.

Многие говорили, говорят и будут говорить, что UDP не надёжный протокол. Давайте оценивать по факту, а факты таковы:

  • На сегодняшний день, гарантии доставки сообщений по UDP почти равны 100%.
  • UDP использует датаграммы. Сообщения не идут в одном потоке.
  • Легковесный=>быстрый.

А теперь давайте перейдём к практике. Наш с вами «сервер» — это демон, написанный на java и запущенный под одним из дистрибутивов Linux, ну или Windows.

Для начала нам нужно создать сам сервер. Это то, что будет создавать канал для клиента, тем самым создавая соединение между сервером и клиентом.

Давайте приступим. Далее будет идти код с пояснениями.

public class Server{
private int port;
public Server(int port){this.port=port}//создаём сервер инициализируя сразу порт, на котором он будет слушать входящие соединения

public void init() throws Exception{
EventLoopGroup workerGroup = new NioEventLoopGroup();//так называемая группа событий, используемая при создании каналов между серверами и клиентом
        try{
Bootstrap bs = new Bootstrap();
            bs.channel(NioDatagramChannel.class)//говорим серверу о том, какой типа канала используется для общения. Тут он является наследником от Channel
            .group(workerGroup)//та самая группа событий
            .handler(new ChannelInitializer<NioDatagramChannel>() //вызывается при каждом подключении, говоря системе о том, что будет использовано для обработки сообщений
             {
                @Override
                public void initChannel(NioDatagramChannel ch) throws Exception {
                    ch.pipeline().addLast(new PlayerHandler());//обработчик входящих сообщений от клиента
                }
            });
         }finally{
workerGroup.shutdownGracefully();
         }
    }
}

Советую использовать StringEncoder и StringDecoder. Упростит вам обработку. На чуть-чуть, но как никак. При UDP он нам не нужен, так как сообщение посылается датаграммой.

TCP версия сервера
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast("decoder", new StringDecoder());//декодирует приходящие данные в строку
                    ch.pipeline().addLast("encoder", new StringEncoder());//кодирует строку в биты при отправке
                     ch.pipeline().addLast(new PlayerHandler());
                 }
             });
            ChannelFuture f = b.bind(port).sync();
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }

Далее у нас есть тот самый PlayerHandler. Он занимается определением игрового события и создания пакета-обработчика, который принимает на вход объект данных. Дальше вы увидите FasDataObject объект. Это специальный класс, написанный мной, который помогает собрать данные в строку в виде action:actionname;key:value;...;key:value;. Его код представлен в конце статьи.

public class PlayerHandler extends SimpleChannelInboundHandler {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object o) throws Exception {
        DatagramPacket data = (DatagramPacket) o;//Переопределяем как пакет датаграмм
        ByteBuf byteBuf = data.content();//получаем данные из датаграммы
        String in = byteBuf.toString(CharsetUtil.UTF_8);//превращаем данные в строку с нужной кодировкой
        FastDataObject fdo = new FastDataObject(in);//создаём объект с данными
        String action = fdo.getParameter("action");//получаем событие, для которого создадим пакет обработчик
        Packet packet = PacketManager.getPacket(action);//Создаем пакет-обработчик
        System.out.println("Packet "+ action +" created");
        packet.setChannel(ctx);//сюда кидаем  объект, с помощью которого будем отправлять обратно
        try {
            packet.handle(fdo);//Обрабатываем пакет, задавая аргументом наши данные
        }catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {//Функция обработок ошибок
        cause.printStackTrace();
    }
}

Идём дальше. Вы увидели PacketManager и Packet. Packet — является абстрактным классом. От него зависимы все пакеты обработчики. Вот он сам Packet класс:

public abstract class Packet{
    private PlayerHandler player;
    private ChannelHandlerContext channelHandlerContext;
    public PlayerHandler getPlayer()
    {
        return player;
    }
    public void setPlayer(PlayerHandler player)
    {
        this.player=player;
    }
    public abstract void write(FastDataObject fastDataObject) throws IOException;//в дальнейшем отправка сообщения клиенту(ам)
    public abstract void handle(FastDataObject fastDataObject) throws IOException;//обработка сообщения
    public void setChannel(ChannelHandlerContext channel){channelHandlerContext=channel;}
    public ChannelHandlerContext getChannelHandlerContext(){return channelHandlerContext;}
}

Пример пакета-обработчика:

Вычислим задержку от клиента до сервера

public class GetPP extends Packet {
    @Override
    public void write(String outputMessage) throws IOException {//Описываем функцию отправки
        System.out.println(outputMessage);
        getChannelHandlerContext().writeAndFlush(outputMessage);
    }

    @Override
    public void handle(FastDataObject fastDataObject) throws IOException {//Описываем функцию обработки
        long time = Long.parseLong(fastDataObject.getString("t"));//Получаем значение по ключу
        long ping = System.currentTimeMillis()-time;//Вычитаем время из текущего
        System.out.println("Ping: "+ping + " ms");
        System.out.println("Writed");
        write("hello");//Отправляем данные клиенту
    }
}

А вот и менеджер пакетов, который создаёт нам нужный обработчик, который зависим от класса Packet. Обработчик создаётся по ключевому слову. Это самое слово является значением ключа action.

public class PacketManager {

    public final static Map<String, Class<? extends Packet>> packets = new HashMap<>();//Карта ключ:значение

    static {
        packets.put("ping", GetPP.class);//Ключ-строка, значение-класс наследник от Packet
    }


    public static Packet getPacket(String action) {//Инстанс обработчика по ключевому слову
        try {
            return packets.get(action).newInstance();
        } catch (IllegalAccessException | InstantiationException ex) {
            ex.printStackTrace();
            return null;
        }
    }
}

Так же есть порты класса на C# и пишется на C++:

FastDataObject

public class FastDataObject {
    private String split=":";
    private String dataName;
    private Map<String, String> params = new HashMap<>();
    private Set keySet;
    public FastDataObject(String data)
    {
        int l1 = data.split(";").length;
        String[] parse = new String[l1];
        parse = data.split(";");
        for (String vals:parse)
        {
            String[] keysik = vals.split(":");
            params.put(keysik[0], keysik[1]);
        }
        keySet=params.keySet();
    }

    public FastDataObject(byte[] bytes)
    {
        String data = new String(bytes, 0, bytes.length);
        int l1 = data.split(";").length;
        String[] parse = new String[l1];
        parse = data.split(";");
        for (String vals:parse)
        {
            String[] keysik = vals.split(":");
            params.put(keysik[0], keysik[1]);
        }
        keySet=params.keySet();
    }


    public FastDataObject(){}

    public void addFloat(String key, Float value)
    {
        params.put(key, value.toString());
        keySet=params.keySet();
    }

    public void addInt(String key, Integer value)
    {
        params.put(key, value.toString());
        keySet=params.keySet();
    }

    public void addString(String key, String value)
    {
        params.put(key, value);
        keySet=params.keySet();
    }

    public void addBoolean(String key, Boolean value)
    {
        params.put(key, value.toString());
        keySet=params.keySet();
    }

    public void addDouble(String key, Double value)
    {
        params.put(key, value.toString());
        keySet=params.keySet();
    }

    public byte[] generateData()
    {
        String result="";
        Collection<String> param = params.values();
        Set<String> keys = params.keySet();
        String[] key = (String[])keys.toArray();
        String[] dataStrings = (String[]) param.toArray();
        for(String string:key)
        {
            result+=string+":"+params.get(key)+";";
        }
        return result.getBytes();
    }

    public String generateString()
    {
        String result="";
        Collection<String> param = params.values();
        Set<String> keys = params.keySet();
        String[] key = (String[])keys.toArray();
        String[] dataStrings = (String[]) param.toArray();
        for(String string:key)
        {
            result+=string+":"+params.get(key)+";";
        }
        return result;
    }

    public String getParameter(int i)
    {
        return params.get(getKey(i));
    }

    public String getParameter(String Key)
    {
        return params.get(Key);
    }

    public float getFloat(String key)
    {
        return Float.parseFloat(params.get(key));
    }

    public float getFloat(int key)
    {
        return Float.parseFloat(params.get(getKey(key)));
    }

    public int getInt(String key)
    {
        return Integer.parseInt(params.get(key));
    }

    public int getInt(int key)
    {
        return Integer.parseInt(params.get(getKey(key)));
    }

    public String getString(String key)
    {
        return params.get(key);
    }

    public String getString(int key)
    {
        return params.get(getKey(key));
    }

    private String getKey(int i)
    {
        String[] keys =  (String[]) keySet.toArray();
        return keys[i];
    }
}

Спасибо за внимание!

Автор: Леонид Якубович

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js