Являясь 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 он нам не нужен, так как сообщение посылается датаграммой.
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++:
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];
}
}
Спасибо за внимание!
Автор: Леонид Якубович