diff --git a/lib/kcp-netty-1.5.0.jar b/lib/kcp-netty-1.5.0.jar deleted file mode 100644 index 3613163bf..000000000 Binary files a/lib/kcp-netty-1.5.0.jar and /dev/null differ diff --git a/lib/kcp.jar b/lib/kcp.jar new file mode 100644 index 000000000..091b59e0c Binary files /dev/null and b/lib/kcp.jar differ diff --git a/src/main/java/emu/grasscutter/netty/KcpChannel.java b/src/main/java/emu/grasscutter/netty/KcpChannel.java deleted file mode 100644 index 2c234ae7c..000000000 --- a/src/main/java/emu/grasscutter/netty/KcpChannel.java +++ /dev/null @@ -1,89 +0,0 @@ -package emu.grasscutter.netty; - -import emu.grasscutter.Grasscutter; -import io.jpower.kcp.netty.UkcpChannel; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; - -public abstract class KcpChannel extends ChannelInboundHandlerAdapter { - private UkcpChannel kcpChannel; - private ChannelHandlerContext ctx; - private boolean isActive; - - public UkcpChannel getChannel() { - return kcpChannel; - } - - public boolean isActive() { - return this.isActive; - } - - @Override - public void channelActive(ChannelHandlerContext ctx) throws Exception { - this.kcpChannel = (UkcpChannel) ctx.channel(); - this.ctx = ctx; - this.isActive = true; - - this.onConnect(); - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception { - this.isActive = false; - - this.onDisconnect(); - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) { - ByteBuf data = (ByteBuf) msg; - onMessage(ctx, data); - } - - @Override - public void channelReadComplete(ChannelHandlerContext ctx) { - ctx.flush(); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - cause.printStackTrace(); - close(); - } - - protected void send(byte[] data) { - if (!isActive()) { - return; - } - ByteBuf packet = Unpooled.wrappedBuffer(data); - kcpChannel.writeAndFlush(packet); - } - - public void close() { - if (getChannel() != null) { - getChannel().close(); - } - } - - /* - protected void logPacket(ByteBuffer buf) { - ByteBuf b = Unpooled.wrappedBuffer(buf.array()); - logPacket(b); - } - */ - - protected void logPacket(ByteBuf buf) { - Grasscutter.getLogger().info("Received: \n" + ByteBufUtil.prettyHexDump(buf)); - } - - // Events - - protected abstract void onConnect(); - - protected abstract void onDisconnect(); - - public abstract void onMessage(ChannelHandlerContext ctx, ByteBuf data); -} diff --git a/src/main/java/emu/grasscutter/netty/KcpHandshaker.java b/src/main/java/emu/grasscutter/netty/KcpHandshaker.java deleted file mode 100644 index 9940402f1..000000000 --- a/src/main/java/emu/grasscutter/netty/KcpHandshaker.java +++ /dev/null @@ -1,85 +0,0 @@ -package emu.grasscutter.netty; - -import java.net.SocketAddress; -import java.nio.channels.SelectableChannel; -import java.util.List; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelConfig; -import io.netty.channel.ChannelMetadata; -import io.netty.channel.ChannelOutboundBuffer; -import io.netty.channel.nio.AbstractNioMessageChannel; - -public class KcpHandshaker extends AbstractNioMessageChannel { - - protected KcpHandshaker(Channel parent, SelectableChannel ch, int readInterestOp) { - super(parent, ch, readInterestOp); - } - - @Override - public ChannelConfig config() { - // TODO Auto-generated method stub - return null; - } - - @Override - public boolean isActive() { - // TODO Auto-generated method stub - return false; - } - - @Override - public ChannelMetadata metadata() { - // TODO Auto-generated method stub - return null; - } - - @Override - protected int doReadMessages(List buf) throws Exception { - // TODO Auto-generated method stub - return 0; - } - - @Override - protected boolean doWriteMessage(Object msg, ChannelOutboundBuffer in) throws Exception { - // TODO Auto-generated method stub - return false; - } - - @Override - protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception { - // TODO Auto-generated method stub - return false; - } - - @Override - protected void doFinishConnect() throws Exception { - // TODO Auto-generated method stub - - } - - @Override - protected SocketAddress localAddress0() { - // TODO Auto-generated method stub - return null; - } - - @Override - protected SocketAddress remoteAddress0() { - // TODO Auto-generated method stub - return null; - } - - @Override - protected void doBind(SocketAddress localAddress) throws Exception { - // TODO Auto-generated method stub - - } - - @Override - protected void doDisconnect() throws Exception { - // TODO Auto-generated method stub - - } - -} diff --git a/src/main/java/emu/grasscutter/netty/KcpServer.java b/src/main/java/emu/grasscutter/netty/KcpServer.java deleted file mode 100644 index 4825a1b85..000000000 --- a/src/main/java/emu/grasscutter/netty/KcpServer.java +++ /dev/null @@ -1,93 +0,0 @@ -package emu.grasscutter.netty; - -import java.net.InetSocketAddress; - -import emu.grasscutter.Grasscutter; -import io.jpower.kcp.netty.ChannelOptionHelper; -import io.jpower.kcp.netty.UkcpChannelOption; -import io.jpower.kcp.netty.UkcpServerChannel; -import io.netty.bootstrap.UkcpServerBootstrap; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; - -@SuppressWarnings("rawtypes") -public class KcpServer extends Thread { - private EventLoopGroup group; - private UkcpServerBootstrap bootstrap; - - private ChannelInitializer serverInitializer; - private InetSocketAddress address; - - public KcpServer(InetSocketAddress address) { - this.address = address; - this.setName("Netty Server Thread"); - } - - public InetSocketAddress getAddress() { - return this.address; - } - - public ChannelInitializer getServerInitializer() { - return serverInitializer; - } - - public void setServerInitializer(ChannelInitializer serverInitializer) { - this.serverInitializer = serverInitializer; - } - - @Override - public void run() { - if (getServerInitializer() == null) { - this.setServerInitializer(new KcpServerInitializer()); - } - - try { - group = new NioEventLoopGroup(); - bootstrap = new UkcpServerBootstrap(); - bootstrap.group(group) - .channel(UkcpServerChannel.class) - .childHandler(this.getServerInitializer()); - ChannelOptionHelper - .nodelay(bootstrap, true, 20, 2, true) - .childOption(UkcpChannelOption.UKCP_MTU, 1200); - - // Start handler - this.onStart(); - - // Start the server. - ChannelFuture f = bootstrap.bind(getAddress()).sync(); - - // Start finish handler - this.onStartFinish(); - - // Wait until the server socket is closed. - f.channel().closeFuture().sync(); - } catch (Exception exception) { - Grasscutter.getLogger().error("Unable to start game server.", exception); - } finally { - // Close - finish(); - } - } - - public void onStart() { - - } - - public void onStartFinish() { - - } - - private void finish() { - try { - group.shutdownGracefully(); - } catch (Exception e) { - - } - Grasscutter.getLogger().info("Game Server closed"); - } -} - - diff --git a/src/main/java/emu/grasscutter/netty/KcpServerInitializer.java b/src/main/java/emu/grasscutter/netty/KcpServerInitializer.java deleted file mode 100644 index 86d871c5f..000000000 --- a/src/main/java/emu/grasscutter/netty/KcpServerInitializer.java +++ /dev/null @@ -1,15 +0,0 @@ -package emu.grasscutter.netty; - -import io.jpower.kcp.netty.UkcpChannel; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; - -@SuppressWarnings("unused") -public class KcpServerInitializer extends ChannelInitializer { - - @Override - protected void initChannel(UkcpChannel ch) throws Exception { - ChannelPipeline pipeline = ch.pipeline(); - } - -} diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index c0560fcc0..4069a2f84 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -21,13 +21,13 @@ import emu.grasscutter.game.tower.TowerScheduleManager; import emu.grasscutter.game.world.World; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; -import emu.grasscutter.netty.KcpServer; import emu.grasscutter.server.event.types.ServerEvent; import emu.grasscutter.server.event.game.ServerTickEvent; import emu.grasscutter.server.event.internal.ServerStartEvent; import emu.grasscutter.server.event.internal.ServerStopEvent; import emu.grasscutter.task.TaskMap; -import emu.grasscutter.BuildConfig; +import kcp.highway.ChannelConfig; +import kcp.highway.KcpServer; import java.net.InetSocketAddress; import java.time.OffsetDateTime; @@ -60,7 +60,7 @@ public final class GameServer extends KcpServer { private final TowerScheduleManager towerScheduleManager; private static InetSocketAddress getAdapterInetSocketAddress(){ - InetSocketAddress inetSocketAddress = null; + InetSocketAddress inetSocketAddress; if(GAME_INFO.bindAddress.equals("")){ inetSocketAddress=new InetSocketAddress(GAME_INFO.bindPort); }else{ @@ -75,9 +75,17 @@ public final class GameServer extends KcpServer { this(getAdapterInetSocketAddress()); } public GameServer(InetSocketAddress address) { - super(address); + ChannelConfig channelConfig = new ChannelConfig(); + channelConfig.nodelay(true,40,2,true); + channelConfig.setMtu(1400); + channelConfig.setSndwnd(256); + channelConfig.setRcvwnd(256); + channelConfig.setTimeoutMillis(30*1000);//30s + channelConfig.setUseConvChannel(true); + channelConfig.setAckNoDelay(false); + + this.init(GameSessionManager.getListener(),channelConfig,address); - this.setServerInitializer(new GameServerInitializer(this)); this.address = address; this.packetHandler = new GameServerPacketHandler(PacketHandler.class); this.questHandler = new ServerQuestHandler(); @@ -96,6 +104,7 @@ public final class GameServer extends KcpServer { this.expeditionManager = new ExpeditionManager(this); this.combineManger = new CombineManger(this); this.towerScheduleManager = new TowerScheduleManager(this); + // Hook into shutdown event. Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown)); } @@ -164,6 +173,7 @@ public final class GameServer extends KcpServer { return towerScheduleManager; } + public TaskMap getTaskMap() { return this.taskMap; } @@ -220,23 +230,23 @@ public final class GameServer extends KcpServer { } return DatabaseHelper.getAccountByName(username); } - - public void onTick() throws Exception { + + public synchronized void onTick(){ Iterator it = this.getWorlds().iterator(); while (it.hasNext()) { World world = it.next(); - + if (world.getPlayerCount() == 0) { it.remove(); } - + world.onTick(); } - + for (Player player : this.getPlayers().values()) { player.onTick(); } - + ServerTickEvent event = new ServerTickEvent(); event.call(); } @@ -249,8 +259,7 @@ public final class GameServer extends KcpServer { } - @Override - public synchronized void start() { + public void start() { // Schedule game loop. Timer gameLoop = new Timer(); gameLoop.scheduleAtFixedRate(new TimerTask() { @@ -263,24 +272,19 @@ public final class GameServer extends KcpServer { } } }, new Date(), 1000L); - - super.start(); - } - - @Override - public void onStartFinish() { Grasscutter.getLogger().info(translate("messages.status.free_software")); Grasscutter.getLogger().info(translate("messages.game.port_bind", Integer.toString(address.getPort()))); - ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now()); event.call(); + ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now()); + event.call(); } - + public void onServerShutdown() { ServerStopEvent event = new ServerStopEvent(ServerEvent.Type.GAME, OffsetDateTime.now()); event.call(); // Kick and save all players List list = new ArrayList<>(this.getPlayers().size()); list.addAll(this.getPlayers().values()); - + for (Player player : list) { player.getSession().close(); } diff --git a/src/main/java/emu/grasscutter/server/game/GameServerInitializer.java b/src/main/java/emu/grasscutter/server/game/GameServerInitializer.java deleted file mode 100644 index 6c48e30c1..000000000 --- a/src/main/java/emu/grasscutter/server/game/GameServerInitializer.java +++ /dev/null @@ -1,22 +0,0 @@ -package emu.grasscutter.server.game; - -import emu.grasscutter.netty.KcpServerInitializer; -import io.jpower.kcp.netty.UkcpChannel; -import io.netty.channel.ChannelPipeline; - -public class GameServerInitializer extends KcpServerInitializer { - private GameServer server; - - public GameServerInitializer(GameServer server) { - this.server = server; - } - - @Override - protected void initChannel(UkcpChannel ch) throws Exception { - ChannelPipeline pipeline=null; - if(ch!=null){ - pipeline = ch.pipeline(); - } - new GameSession(server,pipeline); - } -} diff --git a/src/main/java/emu/grasscutter/server/game/GameSession.java b/src/main/java/emu/grasscutter/server/game/GameSession.java index 397d5ba6f..dc7930f0b 100644 --- a/src/main/java/emu/grasscutter/server/game/GameSession.java +++ b/src/main/java/emu/grasscutter/server/game/GameSession.java @@ -2,9 +2,6 @@ package emu.grasscutter.server.game; import java.io.File; import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.util.Iterator; -import java.util.Map; import java.util.Set; import emu.grasscutter.Grasscutter; @@ -14,23 +11,19 @@ import emu.grasscutter.game.player.Player; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodesUtil; -import emu.grasscutter.netty.KcpChannel; import emu.grasscutter.server.event.game.SendPacketEvent; import emu.grasscutter.utils.Crypto; import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.Utils; -import io.jpower.kcp.netty.UkcpChannel; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPipeline; - -import static emu.grasscutter.utils.Language.translate; import static emu.grasscutter.Configuration.*; +import static emu.grasscutter.utils.Language.translate; -public class GameSession extends KcpChannel { +public class GameSession implements GameSessionManager.KcpChannel { private final GameServer server; - + private GameSessionManager.KcpTunnel tunnel; + private Account account; private Player player; @@ -41,29 +34,10 @@ public class GameSession extends KcpChannel { private long lastPingTime; private int lastClientSeq = 10; - private final ChannelPipeline pipeline; - @Override - public void close() { - setState(SessionState.INACTIVE); - //send disconnection pack in case of reconnection - try { - send(new BasePacket(PacketOpcodes.ServerDisconnectClientNotify)); - }catch (Throwable ignore){ - - } - super.close(); - } public GameSession(GameServer server) { - this(server,null); - } - public GameSession(GameServer server, ChannelPipeline pipeline) { this.server = server; this.state = SessionState.WAITING_FOR_TOKEN; this.lastPingTime = System.currentTimeMillis(); - this.pipeline = pipeline; - if(pipeline!=null) { - pipeline.addLast(this); - } } public GameServer getServer() { @@ -71,10 +45,11 @@ public class GameSession extends KcpChannel { } public InetSocketAddress getAddress() { - if (this.getChannel() == null) { + try{ + return tunnel.getAddress(); + }catch (Throwable ignore){ return null; } - return this.getChannel().remoteAddress(); } public boolean useSecretKey() { @@ -135,37 +110,7 @@ public class GameSession extends KcpChannel { public int getNextClientSequence() { return ++lastClientSeq; } - - @Override - protected void onConnect() { - Grasscutter.getLogger().info(translate("messages.game.connect", this.getAddress().getHostString().toLowerCase())); - } - @Override - protected synchronized void onDisconnect() { // Synchronize so we don't add character at the same time. - Grasscutter.getLogger().info(translate("messages.game.disconnect", this.getAddress().getHostString().toLowerCase())); - - // Set state so no more packets can be handled - this.setState(SessionState.INACTIVE); - - // Save after disconnecting - if (this.isLoggedIn()) { - Player player = getPlayer(); - // Call logout event. - player.onLogout(); - } - try { - pipeline.remove(this); - } catch (Throwable ignore) { - - } - } - - protected void logPacket(ByteBuffer buf) { - ByteBuf b = Unpooled.wrappedBuffer(buf.array()); - logPacket(b); - } - public void replayPacket(int opcode, String name) { String filePath = PACKET(name); File p = new File(filePath); @@ -200,13 +145,16 @@ public class GameSession extends KcpChannel { // Log if (SERVER.debugLevel == ServerDebugMode.ALL) { - logPacket(packet); + if (!loopPacket.contains(packet.getOpcode())) { + Grasscutter.getLogger().info("SEND: " + PacketOpcodesUtil.getOpcodeName(packet.getOpcode()) + " (" + packet.getOpcode() + ")"); + System.out.println(Utils.bytesToHex(packet.getData())); + } } - // Invoke event. SendPacketEvent event = new SendPacketEvent(this, packet); event.call(); - if(!event.isCanceled()) // If event is not cancelled, continue. - this.send(event.getPacket().build()); + if(!event.isCanceled()) { // If event is not cancelled, continue. + tunnel.writeData(event.getPacket().build()); + } } private static final Set loopPacket = Set.of( @@ -217,78 +165,104 @@ public class GameSession extends KcpChannel { PacketOpcodes.QueryPathReq ); - private void logPacket(BasePacket packet) { - if (!loopPacket.contains(packet.getOpcode())) { - Grasscutter.getLogger().info("SEND: " + PacketOpcodesUtil.getOpcodeName(packet.getOpcode()) + " (" + packet.getOpcode() + ")"); - System.out.println(Utils.bytesToHex(packet.getData())); - } - } + @Override + public void onConnected(GameSessionManager.KcpTunnel tunnel) { + this.tunnel = tunnel; + Grasscutter.getLogger().info(translate("messages.game.connect", this.getAddress().toString())); + } + @Override - public void onMessage(ChannelHandlerContext ctx, ByteBuf data) { + public void handleReceive(byte[] bytes) { // Decrypt and turn back into a packet - byte[] byteData = Utils.byteBufToArray(data); - Crypto.xor(byteData, useSecretKey() ? Crypto.ENCRYPT_KEY : Crypto.DISPATCH_KEY); - ByteBuf packet = Unpooled.wrappedBuffer(byteData); - + Crypto.xor(bytes, useSecretKey() ? Crypto.ENCRYPT_KEY : Crypto.DISPATCH_KEY); + ByteBuf packet = Unpooled.wrappedBuffer(bytes); + // Log //logPacket(packet); - // Handle try { + boolean allDebug = SERVER.debugLevel == ServerDebugMode.ALL; while (packet.readableBytes() > 0) { // Length if (packet.readableBytes() < 12) { return; } - // Packet sanity check int const1 = packet.readShort(); if (const1 != 17767) { + if(allDebug){ + Grasscutter.getLogger().error("Bad Data Package Received: got {} ,expect 17767",const1); + } return; // Bad packet } - // Data int opcode = packet.readShort(); int headerLength = packet.readShort(); int payloadLength = packet.readInt(); - byte[] header = new byte[headerLength]; byte[] payload = new byte[payloadLength]; - + packet.readBytes(header); packet.readBytes(payload); - // Sanity check #2 int const2 = packet.readShort(); if (const2 != -30293) { + if(allDebug){ + Grasscutter.getLogger().error("Bad Data Package Received: got {} ,expect -30293",const2); + } return; // Bad packet } - // Log packet - if (SERVER.debugLevel == ServerDebugMode.ALL) { + if (allDebug) { if (!loopPacket.contains(opcode)) { Grasscutter.getLogger().info("RECV: " + PacketOpcodesUtil.getOpcodeName(opcode) + " (" + opcode + ")"); System.out.println(Utils.bytesToHex(payload)); } } - // Handle getServer().getPacketHandler().handle(this, opcode, header, payload); } } catch (Exception e) { e.printStackTrace(); } finally { - data.release(); + //byteBuf.release(); //Needn't packet.release(); } } - + + @Override + public void handleClose() { + setState(SessionState.INACTIVE); + //send disconnection pack in case of reconnection + Grasscutter.getLogger().info(translate("messages.game.disconnect", this.getAddress().toString())); + // Save after disconnecting + if (this.isLoggedIn()) { + Player player = getPlayer(); + // Call logout event. + player.onLogout(); + } + try { + send(new BasePacket(PacketOpcodes.ServerDisconnectClientNotify)); + }catch (Throwable ignore){ + Grasscutter.getLogger().warn("closing {} error",getAddress().getAddress().getHostAddress()); + } + tunnel = null; + } + + public void close() { + tunnel.close(); + } + + public boolean isActive() { + return getState() == SessionState.ACTIVE; + } + public enum SessionState { INACTIVE, WAITING_FOR_TOKEN, WAITING_FOR_LOGIN, PICKING_CHARACTER, - ACTIVE; + ACTIVE } } diff --git a/src/main/java/emu/grasscutter/server/game/GameSessionManager.java b/src/main/java/emu/grasscutter/server/game/GameSessionManager.java new file mode 100644 index 000000000..ed8762938 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/game/GameSessionManager.java @@ -0,0 +1,110 @@ +package emu.grasscutter.server.game; + +import java.net.InetSocketAddress; +import java.util.concurrent.ConcurrentHashMap; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.utils.Crypto; +import emu.grasscutter.utils.Utils; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.DefaultEventLoop; +import kcp.highway.KcpListener; +import kcp.highway.Ukcp; + +public class GameSessionManager { + private static final DefaultEventLoop logicThread = new DefaultEventLoop(); + private static final ConcurrentHashMap sessions = new ConcurrentHashMap<>(); + private static final KcpListener listener = new KcpListener(){ + @Override + public void onConnected(Ukcp ukcp) { + int times = 0; + GameServer server = Grasscutter.getGameServer(); + while (server==null){//Waiting server to establish + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + ukcp.close(); + return; + } + if(times++>5){ + Grasscutter.getLogger().error("Service is not available!"); + ukcp.close(); + return; + } + server = Grasscutter.getGameServer(); + } + GameSession conversation = new GameSession(server); + conversation.onConnected(new KcpTunnel(){ + @Override + public InetSocketAddress getAddress() { + return ukcp.user().getRemoteAddress(); + } + + @Override + public void writeData(byte[] bytes) { + ByteBuf buf = Unpooled.wrappedBuffer(bytes); + ukcp.write(buf); + buf.release(); + } + + @Override + public void close() { + ukcp.close(); + } + + @Override + public int getSrtt() { + return ukcp.srtt(); + } + }); + sessions.put(ukcp,conversation); + } + + @Override + public void handleReceive(ByteBuf buf, Ukcp kcp) { + byte[] byteData = Utils.byteBufToArray(buf); + logicThread.execute(() -> { + try { + GameSession conversation = sessions.get(kcp); + if(conversation!=null) { + conversation.handleReceive(byteData); + } + }catch (Exception e){ + e.printStackTrace(); + } + }); + } + + @Override + public void handleException(Throwable ex, Ukcp ukcp) { + + } + + @Override + public void handleClose(Ukcp ukcp) { + GameSession conversation = sessions.get(ukcp); + if(conversation!=null) { + conversation.handleClose(); + sessions.remove(ukcp); + } + } + }; + + public static KcpListener getListener() { + return listener; + } + + interface KcpTunnel{ + InetSocketAddress getAddress(); + void writeData(byte[] bytes); + void close(); + int getSrtt(); + } + interface KcpChannel{ + void onConnected(KcpTunnel tunnel); + void handleClose(); + void handleReceive(byte[] bytes); + } +}