fix(multiplayer): MultiplayerChannel works.

This commit is contained in:
huanghongxun 2021-09-28 02:05:22 +08:00
parent c66cea41bd
commit fac812f1dd
8 changed files with 283 additions and 155 deletions

View File

@ -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;
}
}

View File

@ -23,9 +23,11 @@ import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.*;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.Socket;
import static org.jackhuang.hmcl.ui.multiplayer.MultiplayerChannel.*;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class MultiplayerClient extends Thread {
@ -64,42 +66,56 @@ public class MultiplayerClient extends Thread {
@Override
public void run() {
LOG.info("Connecting to 127.0.0.1:" + port);
try (Socket socket = new Socket(InetAddress.getLoopbackAddress(), port);
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
for (int i = 0; i < 5; i++) {
try (Socket socket = new Socket(InetAddress.getLoopbackAddress(), port);
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("\n");
writer.write(JsonUtils.UGLY_GSON.toJson(new JoinRequest(MultiplayerManager.CATO_VERSION, id)));
writer.newLine();
writer.flush();
LOG.fine("Send join request with id=" + id);
LOG.fine("Sent join request with id=" + id);
String line = reader.readLine();
if (line == null) {
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) {
String line = reader.readLine();
if (line == null) {
return;
}
}
} catch (IOException | JsonParseException e) {
e.printStackTrace();
} finally {
LOG.info("Lost connection to 127.0.0.1:" + port);
onDisconnected.fireEvent(new Event(this));
JoinResponse response = JsonUtils.fromNonNullJson(line, 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(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 {

View File

@ -65,6 +65,7 @@ public final class MultiplayerManager {
private static final String REMOTE_ADDRESS = "127.0.0.1";
private static final String LOCAL_ADDRESS = "127.0.0.1";
private static final String MODE = "p2p";
private MultiplayerManager() {
}
@ -101,7 +102,8 @@ public final class MultiplayerManager {
"--token", StringUtils.isBlank(token) ? "new" : token,
"--id", peer,
"--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;
try {
process = new ProcessBuilder()
@ -133,7 +135,11 @@ public final class MultiplayerManager {
client.onConnected().register(connectedEvent -> {
try {
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);
} catch (IOException e) {
future.completeExceptionally(e);
@ -157,7 +163,8 @@ public final class MultiplayerManager {
String[] commands = new String[]{exe.toString(),
"--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()
.command(commands)
.start();
@ -214,6 +221,7 @@ public final class MultiplayerManager {
private final String name;
private final State type;
private String id;
private boolean peerConnected = false;
private MultiplayerClient client;
private MultiplayerServer server;
@ -254,14 +262,15 @@ public final class MultiplayerManager {
if (id == null) {
Matcher matcher = TEMP_TOKEN_PATTERN.matcher(log);
if (matcher.find()) {
id = "mix" + matcher.group("id");
id = matcher.group("id");
onIdGenerated.fireEvent(new CatoIdEvent(this, id));
}
}
{
if (!peerConnected) {
Matcher matcher = PEER_CONNECTED_PATTERN.matcher(log);
if (matcher.find()) {
peerConnected = true;
onPeerConnected.fireEvent(new Event(this));
}
}
@ -315,8 +324,8 @@ public final class MultiplayerManager {
return onPeerConnected;
}
private static final Pattern TEMP_TOKEN_PATTERN = Pattern.compile("id\\(mix(?<id>\\w+)\\)");
private static final Pattern PEER_CONNECTED_PATTERN = Pattern.compile("Connection established");
private static final Pattern TEMP_TOKEN_PATTERN = Pattern.compile("id\\((?<id>\\w+)\\)");
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(.*)");
}

View File

@ -53,7 +53,7 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware
private final ReadOnlyObjectWrapper<DiscoveryInfo> natState = new ReadOnlyObjectWrapper<>();
private final ReadOnlyIntegerWrapper gamePort = new ReadOnlyIntegerWrapper(-1);
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.CatoIdEvent> onIdGenerated;
@ -73,7 +73,7 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware
return new MultiplayerPageSkin(this);
}
public ObservableList<MultiplayerServer.CatoClient> getClients() {
public ObservableList<MultiplayerChannel.CatoClient> getClients() {
return clients;
}
@ -236,7 +236,7 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware
});
gamePort.set(session.getClient().getGamePort());
setMultiplayerState(MultiplayerManager.State.CONNECTING);
setMultiplayerState(MultiplayerManager.State.SLAVE);
resolve.run();
}, Platform::runLater).exceptionally(throwable -> {
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"));
}
break;
case -1:
// do nothing
break;
default:
if (!((MultiplayerManager.CatoSession) event.getSource()).isReady()) {
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) {
runInFX(() -> {
token.set(event.getId());
multiplayerState.set(((MultiplayerManager.CatoSession) event.getSource()).getType());
setMultiplayerState(((MultiplayerManager.CatoSession) event.getSource()).getType());
});
}

View File

@ -301,7 +301,7 @@ public class MultiplayerPageSkin extends SkinBase<MultiplayerPage> {
}
private static class ClientItem extends StackPane {
ClientItem(MultiplayerServer.CatoClient client) {
ClientItem(MultiplayerChannel.CatoClient client) {
BorderPane pane = new BorderPane();
pane.setLeft(new Label(client.getUsername()));

View File

@ -21,21 +21,22 @@ import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
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 java.io.*;
import java.net.ServerSocket;
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;
public class MultiplayerServer extends Thread {
private ServerSocket socket;
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) {
this.gamePort = gamePort;
@ -44,15 +45,23 @@ public class MultiplayerServer extends Thread {
setDaemon(true);
}
public EventManager<CatoClient> onClientAdded() {
public EventManager<MultiplayerChannel.CatoClient> onClientAdded() {
return onClientAdded;
}
public EventManager<Event> onKeepAlive() {
return onKeepAlive;
}
public void startServer() throws IOException {
startServer(0);
}
public void startServer(int port) throws IOException {
if (socket != null) {
throw new IllegalStateException("MultiplayerServer already started");
}
socket = new ServerSocket(0);
socket = new ServerSocket(port);
start();
}
@ -79,120 +88,42 @@ public class MultiplayerServer extends Thread {
}
private void handleClient(Socket targetSocket) {
LOG.info("Accepted client " + targetSocket.getRemoteSocketAddress());
try (Socket clientSocket = targetSocket;
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
Request request = JsonUtils.fromNonNullJson(line, Request.class);
request.process(this, writer);
if (isInterrupted()) {
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) {
}
}
@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;
} 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);
}
}
}

View File

@ -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();
}
}

View File

@ -35,6 +35,12 @@ public final class JsonUtils {
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() {
}