fix(multiplayer): MultiplayerChannel works.
This commit is contained in:
@@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.jackhuang.hmcl.ui.multiplayer;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.event.Event;
|
||||||
|
import org.jackhuang.hmcl.util.gson.JsonSubtype;
|
||||||
|
import org.jackhuang.hmcl.util.gson.JsonType;
|
||||||
|
|
||||||
|
public class MultiplayerChannel {
|
||||||
|
|
||||||
|
@JsonType(
|
||||||
|
property = "type",
|
||||||
|
subtypes = {
|
||||||
|
@JsonSubtype(clazz = JoinRequest.class, name = "join"),
|
||||||
|
@JsonSubtype(clazz = KeepAliveRequest.class, name = "keepalive")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
public static class Request {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class JoinRequest extends Request {
|
||||||
|
private final String clientVersion;
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
public JoinRequest(String clientVersion, String username) {
|
||||||
|
this.clientVersion = clientVersion;
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientVersion() {
|
||||||
|
return clientVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class KeepAliveRequest extends Request {
|
||||||
|
private final long timestamp;
|
||||||
|
|
||||||
|
public KeepAliveRequest(long timestamp) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonType(
|
||||||
|
property = "type",
|
||||||
|
subtypes = {
|
||||||
|
@JsonSubtype(clazz = JoinResponse.class, name = "join"),
|
||||||
|
@JsonSubtype(clazz = KeepAliveResponse.class, name = "keepalive")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
public static class Response {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class JoinResponse extends Response {
|
||||||
|
private final int port;
|
||||||
|
|
||||||
|
public JoinResponse(int port) {
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPort() {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class KeepAliveResponse extends Response {
|
||||||
|
private final long timestamp;
|
||||||
|
|
||||||
|
public KeepAliveResponse(long timestamp) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CatoClient extends Event {
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
public CatoClient(Object source, String username) {
|
||||||
|
super(source);
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String verifyJson(String jsonString) {
|
||||||
|
if (jsonString.indexOf('\r') >= 0 || jsonString.indexOf('\n') >= 0) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
return jsonString;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,9 +23,11 @@ import org.jackhuang.hmcl.event.EventManager;
|
|||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
import java.net.ConnectException;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.ui.multiplayer.MultiplayerChannel.*;
|
||||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
|
|
||||||
public class MultiplayerClient extends Thread {
|
public class MultiplayerClient extends Thread {
|
||||||
@@ -64,42 +66,56 @@ public class MultiplayerClient extends Thread {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
LOG.info("Connecting to 127.0.0.1:" + port);
|
LOG.info("Connecting to 127.0.0.1:" + port);
|
||||||
try (Socket socket = new Socket(InetAddress.getLoopbackAddress(), port);
|
for (int i = 0; i < 5; i++) {
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
try (Socket socket = new Socket(InetAddress.getLoopbackAddress(), port);
|
||||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
|
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||||
|
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
|
||||||
|
LOG.info("Connected to 127.0.0.1:" + port);
|
||||||
|
|
||||||
writer.write(JsonUtils.GSON.toJson(new MultiplayerServer.JoinRequest(MultiplayerManager.CATO_VERSION, id)));
|
writer.write(JsonUtils.UGLY_GSON.toJson(new JoinRequest(MultiplayerManager.CATO_VERSION, id)));
|
||||||
writer.write("\n");
|
writer.newLine();
|
||||||
|
writer.flush();
|
||||||
|
|
||||||
LOG.fine("Send join request with id=" + id);
|
LOG.fine("Sent join request with id=" + id);
|
||||||
|
|
||||||
String line = reader.readLine();
|
String line = reader.readLine();
|
||||||
if (line == null) {
|
if (line == null) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
MultiplayerServer.JoinResponse response = JsonUtils.fromNonNullJson(line, MultiplayerServer.JoinResponse.class);
|
|
||||||
setGamePort(response.getPort());
|
|
||||||
onConnected.fireEvent(new ConnectedEvent(this, response.getPort()));
|
|
||||||
|
|
||||||
LOG.fine("Received join response with port " + response.getPort());
|
|
||||||
|
|
||||||
while (!isInterrupted()) {
|
|
||||||
writer.write(JsonUtils.GSON.toJson(new MultiplayerServer.KeepAliveResponse(System.currentTimeMillis())));
|
|
||||||
writer.write("\n");
|
|
||||||
|
|
||||||
try {
|
|
||||||
Thread.sleep(1500);
|
|
||||||
} catch (InterruptedException ignored) {
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
} catch (IOException | JsonParseException e) {
|
JoinResponse response = JsonUtils.fromNonNullJson(line, JoinResponse.class);
|
||||||
e.printStackTrace();
|
setGamePort(response.getPort());
|
||||||
} finally {
|
onConnected.fireEvent(new ConnectedEvent(this, response.getPort()));
|
||||||
LOG.info("Lost connection to 127.0.0.1:" + port);
|
|
||||||
onDisconnected.fireEvent(new Event(this));
|
LOG.fine("Received join response with port " + response.getPort());
|
||||||
|
|
||||||
|
while (!isInterrupted()) {
|
||||||
|
writer.write(verifyJson(JsonUtils.UGLY_GSON.toJson(new KeepAliveResponse(System.currentTimeMillis()))));
|
||||||
|
writer.newLine();
|
||||||
|
writer.flush();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Thread.sleep(1500);
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
LOG.warning("MultiplayerClient interrupted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ConnectException e) {
|
||||||
|
LOG.info("Failed to connect to 127.0.0.1:" + port + ", tried " + i + " time(s)");
|
||||||
|
try {
|
||||||
|
Thread.sleep(2000);
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
LOG.warning("MultiplayerClient interrupted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
} catch (IOException | JsonParseException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
LOG.info("Lost connection to 127.0.0.1:" + port);
|
||||||
|
onDisconnected.fireEvent(new Event(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ConnectedEvent extends Event {
|
public static class ConnectedEvent extends Event {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ public final class MultiplayerManager {
|
|||||||
|
|
||||||
private static final String REMOTE_ADDRESS = "127.0.0.1";
|
private static final String REMOTE_ADDRESS = "127.0.0.1";
|
||||||
private static final String LOCAL_ADDRESS = "127.0.0.1";
|
private static final String LOCAL_ADDRESS = "127.0.0.1";
|
||||||
|
private static final String MODE = "p2p";
|
||||||
|
|
||||||
private MultiplayerManager() {
|
private MultiplayerManager() {
|
||||||
}
|
}
|
||||||
@@ -101,7 +102,8 @@ public final class MultiplayerManager {
|
|||||||
"--token", StringUtils.isBlank(token) ? "new" : token,
|
"--token", StringUtils.isBlank(token) ? "new" : token,
|
||||||
"--id", peer,
|
"--id", peer,
|
||||||
"--local", String.format("%s:%d", LOCAL_ADDRESS, localPort),
|
"--local", String.format("%s:%d", LOCAL_ADDRESS, localPort),
|
||||||
"--remote", String.format("%s:%d", REMOTE_ADDRESS, remotePort)};
|
"--remote", String.format("%s:%d", REMOTE_ADDRESS, remotePort),
|
||||||
|
"--mode", MODE};
|
||||||
Process process;
|
Process process;
|
||||||
try {
|
try {
|
||||||
process = new ProcessBuilder()
|
process = new ProcessBuilder()
|
||||||
@@ -133,7 +135,11 @@ public final class MultiplayerManager {
|
|||||||
client.onConnected().register(connectedEvent -> {
|
client.onConnected().register(connectedEvent -> {
|
||||||
try {
|
try {
|
||||||
int port = findAvailablePort();
|
int port = findAvailablePort();
|
||||||
writer.write(String.format("net add %s %s:%d %s:%d p2p\n", peer, LOCAL_ADDRESS, port, REMOTE_ADDRESS, connectedEvent.getPort()));
|
String command = String.format("net add %s %s:%d %s:%d %s\n", peer, LOCAL_ADDRESS, port, REMOTE_ADDRESS, connectedEvent.getPort(), MODE);
|
||||||
|
LOG.info("Invoking cato: " + command);
|
||||||
|
writer.write(command);
|
||||||
|
writer.newLine();
|
||||||
|
writer.flush();
|
||||||
future.complete(session);
|
future.complete(session);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
future.completeExceptionally(e);
|
future.completeExceptionally(e);
|
||||||
@@ -157,7 +163,8 @@ public final class MultiplayerManager {
|
|||||||
|
|
||||||
String[] commands = new String[]{exe.toString(),
|
String[] commands = new String[]{exe.toString(),
|
||||||
"--token", StringUtils.isBlank(token) ? "new" : token,
|
"--token", StringUtils.isBlank(token) ? "new" : token,
|
||||||
"--allows", String.format("%s:%d/%s:%d", REMOTE_ADDRESS, server.getPort(), REMOTE_ADDRESS, gamePort)};
|
"--allows", String.format("%s:%d/%s:%d", REMOTE_ADDRESS, server.getPort(), REMOTE_ADDRESS, gamePort),
|
||||||
|
"--mode", MODE};
|
||||||
Process process = new ProcessBuilder()
|
Process process = new ProcessBuilder()
|
||||||
.command(commands)
|
.command(commands)
|
||||||
.start();
|
.start();
|
||||||
@@ -214,6 +221,7 @@ public final class MultiplayerManager {
|
|||||||
private final String name;
|
private final String name;
|
||||||
private final State type;
|
private final State type;
|
||||||
private String id;
|
private String id;
|
||||||
|
private boolean peerConnected = false;
|
||||||
private MultiplayerClient client;
|
private MultiplayerClient client;
|
||||||
private MultiplayerServer server;
|
private MultiplayerServer server;
|
||||||
|
|
||||||
@@ -254,14 +262,15 @@ public final class MultiplayerManager {
|
|||||||
if (id == null) {
|
if (id == null) {
|
||||||
Matcher matcher = TEMP_TOKEN_PATTERN.matcher(log);
|
Matcher matcher = TEMP_TOKEN_PATTERN.matcher(log);
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
id = "mix" + matcher.group("id");
|
id = matcher.group("id");
|
||||||
onIdGenerated.fireEvent(new CatoIdEvent(this, id));
|
onIdGenerated.fireEvent(new CatoIdEvent(this, id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
if (!peerConnected) {
|
||||||
Matcher matcher = PEER_CONNECTED_PATTERN.matcher(log);
|
Matcher matcher = PEER_CONNECTED_PATTERN.matcher(log);
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
|
peerConnected = true;
|
||||||
onPeerConnected.fireEvent(new Event(this));
|
onPeerConnected.fireEvent(new Event(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,8 +324,8 @@ public final class MultiplayerManager {
|
|||||||
return onPeerConnected;
|
return onPeerConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Pattern TEMP_TOKEN_PATTERN = Pattern.compile("id\\(mix(?<id>\\w+)\\)");
|
private static final Pattern TEMP_TOKEN_PATTERN = Pattern.compile("id\\((?<id>\\w+)\\)");
|
||||||
private static final Pattern PEER_CONNECTED_PATTERN = Pattern.compile("Connection established");
|
private static final Pattern PEER_CONNECTED_PATTERN = Pattern.compile("Connected to main net");
|
||||||
private static final Pattern LOG_PATTERN = Pattern.compile("(\\[\\d+])\\s+(\\w+)\\s+(\\w+-{0,1}\\w+):\\s(.*)");
|
private static final Pattern LOG_PATTERN = Pattern.compile("(\\[\\d+])\\s+(\\w+)\\s+(\\w+-{0,1}\\w+):\\s(.*)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware
|
|||||||
private final ReadOnlyObjectWrapper<DiscoveryInfo> natState = new ReadOnlyObjectWrapper<>();
|
private final ReadOnlyObjectWrapper<DiscoveryInfo> natState = new ReadOnlyObjectWrapper<>();
|
||||||
private final ReadOnlyIntegerWrapper gamePort = new ReadOnlyIntegerWrapper(-1);
|
private final ReadOnlyIntegerWrapper gamePort = new ReadOnlyIntegerWrapper(-1);
|
||||||
private final ReadOnlyObjectWrapper<MultiplayerManager.CatoSession> session = new ReadOnlyObjectWrapper<>();
|
private final ReadOnlyObjectWrapper<MultiplayerManager.CatoSession> session = new ReadOnlyObjectWrapper<>();
|
||||||
private final ObservableList<MultiplayerServer.CatoClient> clients = FXCollections.observableArrayList();
|
private final ObservableList<MultiplayerChannel.CatoClient> clients = FXCollections.observableArrayList();
|
||||||
|
|
||||||
private Consumer<MultiplayerManager.CatoExitEvent> onExit;
|
private Consumer<MultiplayerManager.CatoExitEvent> onExit;
|
||||||
private Consumer<MultiplayerManager.CatoIdEvent> onIdGenerated;
|
private Consumer<MultiplayerManager.CatoIdEvent> onIdGenerated;
|
||||||
@@ -73,7 +73,7 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware
|
|||||||
return new MultiplayerPageSkin(this);
|
return new MultiplayerPageSkin(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableList<MultiplayerServer.CatoClient> getClients() {
|
public ObservableList<MultiplayerChannel.CatoClient> getClients() {
|
||||||
return clients;
|
return clients;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +236,7 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware
|
|||||||
});
|
});
|
||||||
|
|
||||||
gamePort.set(session.getClient().getGamePort());
|
gamePort.set(session.getClient().getGamePort());
|
||||||
setMultiplayerState(MultiplayerManager.State.CONNECTING);
|
setMultiplayerState(MultiplayerManager.State.SLAVE);
|
||||||
resolve.run();
|
resolve.run();
|
||||||
}, Platform::runLater).exceptionally(throwable -> {
|
}, Platform::runLater).exceptionally(throwable -> {
|
||||||
LOG.log(Level.WARNING, "Failed to join sessoin");
|
LOG.log(Level.WARNING, "Failed to join sessoin");
|
||||||
@@ -314,6 +314,9 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware
|
|||||||
Controllers.dialog(i18n("multiplayer.exit.timeout"));
|
Controllers.dialog(i18n("multiplayer.exit.timeout"));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case -1:
|
||||||
|
// do nothing
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
if (!((MultiplayerManager.CatoSession) event.getSource()).isReady()) {
|
if (!((MultiplayerManager.CatoSession) event.getSource()).isReady()) {
|
||||||
Controllers.dialog(i18n("multiplayer.exit.before_ready", event.getExitCode()));
|
Controllers.dialog(i18n("multiplayer.exit.before_ready", event.getExitCode()));
|
||||||
@@ -335,7 +338,7 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware
|
|||||||
private void onCatoIdGenerated(MultiplayerManager.CatoIdEvent event) {
|
private void onCatoIdGenerated(MultiplayerManager.CatoIdEvent event) {
|
||||||
runInFX(() -> {
|
runInFX(() -> {
|
||||||
token.set(event.getId());
|
token.set(event.getId());
|
||||||
multiplayerState.set(((MultiplayerManager.CatoSession) event.getSource()).getType());
|
setMultiplayerState(((MultiplayerManager.CatoSession) event.getSource()).getType());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ public class MultiplayerPageSkin extends SkinBase<MultiplayerPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class ClientItem extends StackPane {
|
private static class ClientItem extends StackPane {
|
||||||
ClientItem(MultiplayerServer.CatoClient client) {
|
ClientItem(MultiplayerChannel.CatoClient client) {
|
||||||
BorderPane pane = new BorderPane();
|
BorderPane pane = new BorderPane();
|
||||||
pane.setLeft(new Label(client.getUsername()));
|
pane.setLeft(new Label(client.getUsername()));
|
||||||
|
|
||||||
|
|||||||
@@ -21,21 +21,22 @@ import com.google.gson.JsonParseException;
|
|||||||
import org.jackhuang.hmcl.event.Event;
|
import org.jackhuang.hmcl.event.Event;
|
||||||
import org.jackhuang.hmcl.event.EventManager;
|
import org.jackhuang.hmcl.event.EventManager;
|
||||||
import org.jackhuang.hmcl.util.Lang;
|
import org.jackhuang.hmcl.util.Lang;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonSubtype;
|
|
||||||
import org.jackhuang.hmcl.util.gson.JsonType;
|
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.ui.multiplayer.MultiplayerChannel.*;
|
||||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
|
|
||||||
public class MultiplayerServer extends Thread {
|
public class MultiplayerServer extends Thread {
|
||||||
private ServerSocket socket;
|
private ServerSocket socket;
|
||||||
private final int gamePort;
|
private final int gamePort;
|
||||||
|
|
||||||
private final EventManager<CatoClient> onClientAdded = new EventManager<CatoClient>();
|
private final EventManager<MultiplayerChannel.CatoClient> onClientAdded = new EventManager<>();
|
||||||
|
private final EventManager<Event> onKeepAlive = new EventManager<>();
|
||||||
|
|
||||||
public MultiplayerServer(int gamePort) {
|
public MultiplayerServer(int gamePort) {
|
||||||
this.gamePort = gamePort;
|
this.gamePort = gamePort;
|
||||||
@@ -44,15 +45,23 @@ public class MultiplayerServer extends Thread {
|
|||||||
setDaemon(true);
|
setDaemon(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public EventManager<CatoClient> onClientAdded() {
|
public EventManager<MultiplayerChannel.CatoClient> onClientAdded() {
|
||||||
return onClientAdded;
|
return onClientAdded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public EventManager<Event> onKeepAlive() {
|
||||||
|
return onKeepAlive;
|
||||||
|
}
|
||||||
|
|
||||||
public void startServer() throws IOException {
|
public void startServer() throws IOException {
|
||||||
|
startServer(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startServer(int port) throws IOException {
|
||||||
if (socket != null) {
|
if (socket != null) {
|
||||||
throw new IllegalStateException("MultiplayerServer already started");
|
throw new IllegalStateException("MultiplayerServer already started");
|
||||||
}
|
}
|
||||||
socket = new ServerSocket(0);
|
socket = new ServerSocket(port);
|
||||||
|
|
||||||
start();
|
start();
|
||||||
}
|
}
|
||||||
@@ -79,120 +88,42 @@ public class MultiplayerServer extends Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleClient(Socket targetSocket) {
|
private void handleClient(Socket targetSocket) {
|
||||||
|
LOG.info("Accepted client " + targetSocket.getRemoteSocketAddress());
|
||||||
try (Socket clientSocket = targetSocket;
|
try (Socket clientSocket = targetSocket;
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
|
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
|
||||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
|
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
|
||||||
String line;
|
String line;
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
Request request = JsonUtils.fromNonNullJson(line, Request.class);
|
if (isInterrupted()) {
|
||||||
request.process(this, writer);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.fine("Message from client " + targetSocket.getRemoteSocketAddress() + ":" + line);
|
||||||
|
MultiplayerChannel.Request request = JsonUtils.fromNonNullJson(line, MultiplayerChannel.Request.class);
|
||||||
|
|
||||||
|
if (request instanceof JoinRequest) {
|
||||||
|
JoinRequest joinRequest = (JoinRequest) request;
|
||||||
|
LOG.info("Received join request with clientVersion=" + joinRequest.getClientVersion() + ", id=" + joinRequest.getUsername());
|
||||||
|
|
||||||
|
writer.write(verifyJson(JsonUtils.UGLY_GSON.toJson(new JoinResponse(gamePort))));
|
||||||
|
writer.newLine();
|
||||||
|
writer.flush();
|
||||||
|
|
||||||
|
onClientAdded.fireEvent(new CatoClient(this, joinRequest.getUsername()));
|
||||||
|
} else if (request instanceof KeepAliveRequest) {
|
||||||
|
writer.write(JsonUtils.UGLY_GSON.toJson(new KeepAliveResponse(System.currentTimeMillis())));
|
||||||
|
writer.newLine();
|
||||||
|
writer.flush();
|
||||||
|
|
||||||
|
onKeepAlive.fireEvent(new Event(this));
|
||||||
|
} else {
|
||||||
|
LOG.log(Level.WARNING, "Unrecognized packet from client " + targetSocket.getRemoteSocketAddress() + ":" + line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException | JsonParseException ignored) {
|
} catch (IOException e) {
|
||||||
}
|
LOG.log(Level.WARNING, "Failed to handle client socket.", e);
|
||||||
}
|
} catch (JsonParseException e) {
|
||||||
|
LOG.log(Level.SEVERE, "Failed to parse client request. This should not happen.", e);
|
||||||
@JsonType(
|
|
||||||
property = "type",
|
|
||||||
subtypes = {
|
|
||||||
@JsonSubtype(clazz = JoinRequest.class, name = "join"),
|
|
||||||
@JsonSubtype(clazz = KeepAliveRequest.class, name = "keepalive")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
public static class Request {
|
|
||||||
|
|
||||||
public void process(MultiplayerServer server, BufferedWriter writer) throws IOException, JsonParseException {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class JoinRequest extends Request {
|
|
||||||
private final String clientVersion;
|
|
||||||
private final String username;
|
|
||||||
|
|
||||||
public JoinRequest(String clientVersion, String username) {
|
|
||||||
this.clientVersion = clientVersion;
|
|
||||||
this.username = username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getClientVersion() {
|
|
||||||
return clientVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUsername() {
|
|
||||||
return username;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void process(MultiplayerServer server, BufferedWriter writer) throws IOException, JsonParseException {
|
|
||||||
LOG.fine("Received join request with clientVersion=" + clientVersion + ", id=" + username);
|
|
||||||
|
|
||||||
writer.write(JsonUtils.GSON.toJson(new JoinResponse(server.gamePort)));
|
|
||||||
|
|
||||||
server.onClientAdded.fireEvent(new CatoClient(server, username));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class KeepAliveRequest extends Request {
|
|
||||||
private final long timestamp;
|
|
||||||
|
|
||||||
public KeepAliveRequest(long timestamp) {
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getTimestamp() {
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void process(MultiplayerServer server, BufferedWriter writer) throws IOException, JsonParseException {
|
|
||||||
writer.write(JsonUtils.GSON.toJson(new KeepAliveResponse(System.currentTimeMillis())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonType(
|
|
||||||
property = "type",
|
|
||||||
subtypes = {
|
|
||||||
@JsonSubtype(clazz = JoinResponse.class, name = "join"),
|
|
||||||
@JsonSubtype(clazz = KeepAliveResponse.class, name = "keepalive")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
public static class Response {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class JoinResponse extends Response {
|
|
||||||
private final int port;
|
|
||||||
|
|
||||||
public JoinResponse(int port) {
|
|
||||||
this.port = port;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPort() {
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class KeepAliveResponse extends Response {
|
|
||||||
private final long timestamp;
|
|
||||||
|
|
||||||
public KeepAliveResponse(long timestamp) {
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getTimestamp() {
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class CatoClient extends Event {
|
|
||||||
private final String username;
|
|
||||||
|
|
||||||
public CatoClient(Object source, String username) {
|
|
||||||
super(source);
|
|
||||||
this.username = username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUsername() {
|
|
||||||
return username;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.jackhuang.hmcl.ui.multiplayer;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.util.Logging;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class MultiplayerClientServerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Ignore
|
||||||
|
public void startServer() throws Exception {
|
||||||
|
Logging.initForTest();
|
||||||
|
MultiplayerServer server = new MultiplayerServer(1000);
|
||||||
|
server.startServer(44444);
|
||||||
|
|
||||||
|
MultiplayerClient client = new MultiplayerClient("username", 44444);
|
||||||
|
client.start();
|
||||||
|
|
||||||
|
server.onKeepAlive().register(event -> {
|
||||||
|
client.interrupt();
|
||||||
|
server.interrupt();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,12 @@ public final class JsonUtils {
|
|||||||
|
|
||||||
public static final Gson GSON = defaultGsonBuilder().create();
|
public static final Gson GSON = defaultGsonBuilder().create();
|
||||||
|
|
||||||
|
public static final Gson UGLY_GSON = new GsonBuilder()
|
||||||
|
.registerTypeAdapterFactory(JsonTypeAdapterFactory.INSTANCE)
|
||||||
|
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
|
||||||
|
.registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory.INSTANCE)
|
||||||
|
.create();
|
||||||
|
|
||||||
private JsonUtils() {
|
private JsonUtils() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user