diff --git a/tunnel-server/pom.xml b/tunnel-server/pom.xml
index f32494492..259a24683 100644
--- a/tunnel-server/pom.xml
+++ b/tunnel-server/pom.xml
@@ -55,6 +55,11 @@
spring-boot-starter-actuator
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
org.springframework.boot
spring-boot-starter-security
diff --git a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/RedisTunnelClusterStore.java b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/RedisTunnelClusterStore.java
new file mode 100644
index 000000000..449ea84cd
--- /dev/null
+++ b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/RedisTunnelClusterStore.java
@@ -0,0 +1,95 @@
+package com.alibaba.arthas.tunnel.server;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+
+/**
+ *
+ * @author hengyunabc 2020-10-27
+ *
+ */
+public class RedisTunnelClusterStore implements TunnelClusterStore {
+
+ private String prefix = "arthas-tunnel-agent-";
+
+ private StringRedisTemplate redisTemplate;
+
+ @Override
+ public String findHost(String agentId) {
+ ValueOperations opsForValue = redisTemplate.opsForValue();
+
+ return opsForValue.get(prefix + agentId);
+ }
+
+ @Override
+ public void removeAgent(String agentId) {
+ ValueOperations opsForValue = redisTemplate.opsForValue();
+ opsForValue.getOperations().delete(prefix + agentId);
+ }
+
+ @Override
+ public void addHost(String agentId, String host, long timeout, TimeUnit timeUnit) {
+ ValueOperations opsForValue = redisTemplate.opsForValue();
+
+ opsForValue.set(prefix + agentId, host, timeout, timeUnit);
+ }
+
+ public StringRedisTemplate getRedisTemplate() {
+ return redisTemplate;
+ }
+
+ public void setRedisTemplate(StringRedisTemplate redisTemplate) {
+ this.redisTemplate = redisTemplate;
+ }
+
+ @Override
+ public Collection allAgentIds() {
+ ValueOperations opsForValue = redisTemplate.opsForValue();
+
+ Set result = new HashSet();
+
+ int length = prefix.length();
+ for (String value : opsForValue.getOperations().keys(prefix + "*")) {
+ result.add(value.substring(length));
+
+ }
+ return result;
+ }
+
+ @Override
+ public Collection> agentInfo(String appName) {
+ ValueOperations opsForValue = redisTemplate.opsForValue();
+
+ Set keys = new HashSet();
+
+ String prefixWithAppName = prefix + appName + "|";
+
+ for (String value : opsForValue.getOperations().keys(prefixWithAppName + "*")) {
+ keys.add(value);
+
+ }
+
+ List values = opsForValue.getOperations().opsForValue().multiGet(keys);
+
+ Collection> result = new HashSet<>();
+
+ Iterator iterator = values.iterator();
+
+ for (String key : keys) {
+ String host = iterator.next();
+ String agentId = key.substring(prefix.length());
+ result.add(Pair.of(agentId, host));
+ }
+
+ return result;
+ }
+
+}
diff --git a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelClusterStore.java b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelClusterStore.java
new file mode 100644
index 000000000..21f959f42
--- /dev/null
+++ b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelClusterStore.java
@@ -0,0 +1,24 @@
+package com.alibaba.arthas.tunnel.server;
+
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang3.tuple.Pair;
+
+/**
+ * 保存agentId连接到哪个具体的 tunnel server,集群部署时使用
+ *
+ * @author hengyunabc 2020-10-27
+ *
+ */
+public interface TunnelClusterStore {
+ public void addHost(String agentId, String host, long expire, TimeUnit timeUnit);
+
+ public String findHost(String agentId);
+
+ public void removeAgent(String agentId);
+
+ public Collection allAgentIds();
+
+ public Collection> agentInfo(String appName);
+}
diff --git a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelServer.java b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelServer.java
index bff3bedab..88437259b 100644
--- a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelServer.java
+++ b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelServer.java
@@ -1,6 +1,7 @@
package com.alibaba.arthas.tunnel.server;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@@ -10,6 +11,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.arthas.tunnel.common.SimpleHttpResponse;
+import com.alibaba.arthas.tunnel.server.utils.InetAddressUtil;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
@@ -31,10 +33,12 @@ import io.netty.util.concurrent.Promise;
*/
public class TunnelServer {
private final static Logger logger = LoggerFactory.getLogger(TunnelServer.class);
+ public static final String DEFAULT_WEBSOCKET_PATH = "/ws";
private boolean ssl;
private String host;
private int port;
+ private String path = DEFAULT_WEBSOCKET_PATH;
private Map agentInfoMap = new ConcurrentHashMap();
@@ -50,6 +54,16 @@ public class TunnelServer {
private Channel channel;
+ /**
+ * 在集群部署时,保存agentId和host关系
+ */
+ private TunnelClusterStore tunnelClusterStore;
+
+ /**
+ * 集群部署时外部连接的host
+ */
+ private String clientConnectHost;
+
public void start() throws Exception {
// Configure SSL.
final SslContext sslCtx;
@@ -78,6 +92,17 @@ public class TunnelServer {
agentInfoMap.entrySet().removeIf(e -> !e.getValue().getChannelHandlerContext().channel().isActive());
clientConnectionInfoMap.entrySet()
.removeIf(e -> !e.getValue().getChannelHandlerContext().channel().isActive());
+
+ // 更新集群key信息
+ if (tunnelClusterStore != null && clientConnectHost != null) {
+ try {
+ for (Entry entry : agentInfoMap.entrySet()) {
+ tunnelClusterStore.addHost(entry.getKey(), clientConnectHost, 60 * 60, TimeUnit.SECONDS);
+ }
+ } catch (Throwable t) {
+ logger.error("update tunnel info error", t);
+ }
+ }
}
}, 60, 60, TimeUnit.SECONDS);
@@ -97,10 +122,17 @@ public class TunnelServer {
public void addAgent(String id, AgentInfo agentInfo) {
agentInfoMap.put(id, agentInfo);
+ if (this.tunnelClusterStore != null) {
+ this.tunnelClusterStore.addHost(id, clientConnectHost, 60 * 60, TimeUnit.SECONDS);
+ }
}
public AgentInfo removeAgent(String id) {
- return agentInfoMap.remove(id);
+ AgentInfo agentInfo = agentInfoMap.remove(id);
+ if (this.tunnelClusterStore != null) {
+ this.tunnelClusterStore.removeAgent(id);
+ }
+ return agentInfo;
}
public Optional findClientConnection(String id) {
@@ -175,4 +207,28 @@ public class TunnelServer {
public void setClientConnectionInfoMap(Map clientConnectionInfoMap) {
this.clientConnectionInfoMap = clientConnectionInfoMap;
}
+
+ public TunnelClusterStore getTunnelClusterStore() {
+ return tunnelClusterStore;
+ }
+
+ public void setTunnelClusterStore(TunnelClusterStore tunnelClusterStore) {
+ this.tunnelClusterStore = tunnelClusterStore;
+ }
+
+ public String getClientConnectHost() {
+ return clientConnectHost;
+ }
+
+ public void setClientConnectHost(String clientConnectHost) {
+ this.clientConnectHost = clientConnectHost;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
}
diff --git a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelSocketServerInitializer.java b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelSocketServerInitializer.java
index de20c094e..8ea047cf2 100644
--- a/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelSocketServerInitializer.java
+++ b/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/TunnelSocketServerInitializer.java
@@ -18,8 +18,6 @@ import io.netty.handler.ssl.SslContext;
*/
public class TunnelSocketServerInitializer extends ChannelInitializer {
- private static final String WEBSOCKET_PATH = "/ws";
-
private final SslContext sslCtx;
private TunnelServer tunnelServer;
@@ -38,7 +36,7 @@ public class TunnelSocketServerInitializer extends ChannelInitializer
+ * 在超过一块网卡时会有问题,因为这里每次都只是取了第一块网卡绑定的IP地址
+ */
+ public static String getInetAddress() {
+ try {
+ Enumeration interfaces = NetworkInterface.getNetworkInterfaces();
+ InetAddress address = null;
+ while (interfaces.hasMoreElements()) {
+ NetworkInterface ni = interfaces.nextElement();
+ Enumeration addresses = ni.getInetAddresses();
+ while (addresses.hasMoreElements()) {
+ address = addresses.nextElement();
+ if (isValidAddress(address)) {
+ return address.getHostAddress();
+ }
+ }
+ }
+ logger.warn("Can not get the server IP address");
+ return null;
+ } catch (Throwable t) {
+ logger.error("Can not get the server IP address", t);
+ return null;
+ }
+ }
+
+ public static boolean isValidAddress(InetAddress address) {
+ return address != null && !address.isLoopbackAddress() // filter 127.x.x.x
+ && !address.isAnyLocalAddress() // filter 0.0.0.0
+ && !address.isLinkLocalAddress() // filter 169.254.0.0/16
+ && !address.getHostAddress().contains(":");// filter IPv6
+ }
+}
diff --git a/tunnel-server/src/main/resources/static/web-console.js b/tunnel-server/src/main/resources/static/web-console.js
index d1388c034..fb8401c58 100644
--- a/tunnel-server/src/main/resources/static/web-console.js
+++ b/tunnel-server/src/main/resources/static/web-console.js
@@ -77,10 +77,10 @@ function getTerminalSize () {
}
/** init websocket **/
-function initWs (ip, port, agentId) {
+function initWs (ip, port, path, agentId) {
var protocol= location.protocol === 'https:' ? 'wss://' : 'ws://';
- var path = protocol + ip + ':' + port + '/ws?method=connectArthas&id=' + agentId;
- ws = new WebSocket(path);
+ var uri = protocol + ip + ':' + port + '/' + path + '?method=connectArthas&id=' + agentId;
+ ws = new WebSocket(uri);
}
/** init xterm **/
@@ -114,8 +114,13 @@ function startConnect (silent) {
alert('Already connected');
return;
}
+
+ var path = getUrlParam('path');
+ if (path == null) {
+ path = "ws";
+ }
// init webSocket
- initWs(ip, port, agentId);
+ initWs(ip, port, path, agentId);
ws.onerror = function () {
ws.close();
ws = null;