mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-12-19 18:34:49 +01:00
[Ready]Replace deprecated KCP library (#1237)
* Replace deprecated KCP library support get srtt Waiting server to establish logicThread Print Bad Package Information Avoid orphan data improve conv id security * Improve connection subsequence
This commit is contained in:
@@ -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<World> 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<Player> list = new ArrayList<>(this.getPlayers().size());
|
||||
list.addAll(this.getPlayers().values());
|
||||
|
||||
|
||||
for (Player player : list) {
|
||||
player.getSession().close();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Integer> 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Ukcp,GameSession> 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user