mirror of
https://github.com/tiennm99/zfoo.git
synced 2026-10-03 09:14:01 +00:00
docs: Translate the Chinese comments in the event module into English.
This commit is contained in:
1 parent
8460fd7d15
commit
9596782ec7
119 files changed
+854
-871
No files matched your search
@@ -51,14 +51,16 @@ public class NetContext implements ApplicationListener<ApplicationContextEvent>,
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
/**
|
||||
* 使用Router时,使用的send方法的第1个参数指定session发送消息
|
||||
* 如:创建了一个TcpClient,那明确得到了这个session,则可以通过这个session发送消息
|
||||
* When using the Router, the first parameter of the send() method specifies the session for sending messages.
|
||||
* For example: after creating a TcpClient and obtaining its session, messages can be sent through that session.
|
||||
*/
|
||||
private IRouter router;
|
||||
|
||||
/**
|
||||
* 使用Consumer时,提供的接口是没有Session参数的,都是通过负载均衡策略从而拿到ConsumerSession,然后再发送消息。
|
||||
* 如:通过web端向游戏服务器发送请求,web端压根没保存任何session,因此就要通过Consumer去请求
|
||||
* When using the Consumer, the provided interface has no Session parameter.
|
||||
* Sessions are obtained via the load-balancing strategy (ConsumerSession) and messages are sent through it.
|
||||
* For example: when a web client sends a request to a game server, the web side holds no session,
|
||||
* so it must use Consumer to make the request.
|
||||
*/
|
||||
private IConsumer consumer;
|
||||
|
||||
@@ -126,18 +128,18 @@ public class NetContext implements ApplicationListener<ApplicationContextEvent>,
|
||||
}
|
||||
|
||||
public synchronized void shutdownAfter() {
|
||||
// 关闭zookeeper的客户端
|
||||
// Close the ZooKeeper client
|
||||
configManager.getRegistry().shutdown();
|
||||
|
||||
// 先关闭所有session
|
||||
// Close all sessions first
|
||||
sessionManager.forEachClientSession(it -> IOUtils.closeIO(it));
|
||||
sessionManager.forEachServerSession(it -> IOUtils.closeIO(it));
|
||||
|
||||
// 关闭客户端和服务器
|
||||
// Shut down clients and servers
|
||||
AbstractClient.shutdown();
|
||||
AbstractServer.shutdownAllServers();
|
||||
|
||||
// 关闭TaskBus
|
||||
// Shut down TaskBus
|
||||
try {
|
||||
Field field = TaskBus.class.getDeclaredField("executors");
|
||||
ReflectionUtils.makeAccessible(field);
|
||||
|
||||
@@ -33,12 +33,12 @@ public class ConfigManager implements IConfigManager {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConfigManager.class);
|
||||
|
||||
/**
|
||||
* 本地配置
|
||||
* Local network configuration for this node.
|
||||
*/
|
||||
private NetConfig localConfig;
|
||||
|
||||
/**
|
||||
* 注册中心
|
||||
* Service registry used for provider registration and discovery.
|
||||
*/
|
||||
private IRegistry registry;
|
||||
|
||||
@@ -52,16 +52,17 @@ public class ConfigManager implements IConfigManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在调用这个方法之前:localConfig也就是NetConfig虽然已经被注册到Spring容器中,但是模块id等信息依然是没有的。这需要从protocol.xml中读取覆盖才行
|
||||
* Before this method is called, localConfig (NetConfig) has been registered in the Spring container
|
||||
* but module IDs and versions are not yet populated — they must be read from protocol.xml.
|
||||
*/
|
||||
@Override
|
||||
public void initRegistry() {
|
||||
// 通过protocol,写入provider的module的id和version
|
||||
// Populate provider module IDs and versions from the protocol configuration
|
||||
var providerConfig = localConfig.getProvider();
|
||||
if (providerConfig != null && CollectionUtils.isNotEmpty(providerConfig.getProviders())) {
|
||||
// 服务提供者名字Set列表
|
||||
// Set of declared provider names used for duplicate detection
|
||||
var providerSet = new HashSet<String>();
|
||||
// 检查并且替换配置文件中的ProtocolModule
|
||||
// Validate and cross-reference each ProtocolModule entry in the config
|
||||
for (var provider : providerConfig.getProviders()) {
|
||||
var protocolModule = provider.getProtocolModule();
|
||||
var providerName = provider.getProvider();
|
||||
@@ -72,17 +73,17 @@ public class ConfigManager implements IConfigManager {
|
||||
|
||||
var consumerConfig = localConfig.getConsumer();
|
||||
if (Objects.nonNull(consumerConfig) && CollectionUtils.isNotEmpty(consumerConfig.getConsumers())) {
|
||||
// 服务消费者名字Set列表
|
||||
// Set of declared consumer names used for duplicate detection
|
||||
var consumerSet = new HashSet<String>();
|
||||
for (var consumerModule : consumerConfig.getConsumers()) {
|
||||
// 提供的接口实现 提供者名
|
||||
// The provider name that this consumer subscribes to
|
||||
var consumer = consumerModule.getConsumer();
|
||||
AssertionUtils.isTrue(consumerSet.add(consumer), "consumer:[{}] has duplicate consumer module", consumer);
|
||||
}
|
||||
}
|
||||
|
||||
// 走到这之后,NetConfig通过app.xml(读取有哪些消费者)+protocol.xml(模块号信息)完成了初始化
|
||||
// 接下来就是通过注册中心,把生产者和消费者关联起来
|
||||
// At this point, NetConfig is fully initialised via app.xml (consumers) + protocol.xml (module IDs)
|
||||
// The registry now links providers and consumers together
|
||||
try {
|
||||
var registryConfig = NetContext.getConfigManager().getLocalConfig().getRegistry();
|
||||
if (registryConfig != null) {
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
|
||||
package com.zfoo.net.config.model;
|
||||
|
||||
import com.zfoo.protocol.registration.ProtocolModule;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -21,10 +19,10 @@ import java.util.Objects;
|
||||
*/
|
||||
public class ConsumerModule {
|
||||
|
||||
// 负载均衡方式
|
||||
// Load balancing strategy
|
||||
private String loadBalancer;
|
||||
|
||||
// 消费哪个provider
|
||||
// The provider name this consumer subscribes to
|
||||
private String consumer;
|
||||
|
||||
public ConsumerModule() {
|
||||
|
||||
@@ -36,27 +36,27 @@ public class NetConfig {
|
||||
private String protocolParam;
|
||||
|
||||
/**
|
||||
* 生成协议列表
|
||||
* The list of code languages for which protocols should be generated.
|
||||
*/
|
||||
private String codeLanguages;
|
||||
|
||||
/**
|
||||
* 注册中心
|
||||
* Registry (e.g., ZooKeeper) configuration.
|
||||
*/
|
||||
private RegistryConfig registry;
|
||||
|
||||
/**
|
||||
* 监控
|
||||
* Monitoring configuration.
|
||||
*/
|
||||
private MonitorConfig monitor;
|
||||
|
||||
/**
|
||||
* 生产者配置
|
||||
* Provider (server-side) configuration.
|
||||
*/
|
||||
private ProviderConfig provider;
|
||||
|
||||
/**
|
||||
* 消费者配置
|
||||
* Consumer (client-side) configuration.
|
||||
*/
|
||||
private ConsumerConfig consumer;
|
||||
|
||||
|
||||
@@ -22,12 +22,12 @@ import java.util.Objects;
|
||||
public class ProviderModule {
|
||||
|
||||
/**
|
||||
* 模块id和模块名
|
||||
* The module ID and module name.
|
||||
*/
|
||||
private String protocolModule;
|
||||
|
||||
/**
|
||||
* 提供者名字
|
||||
* The name of the service provider.
|
||||
*/
|
||||
private String provider;
|
||||
|
||||
|
||||
@@ -43,9 +43,9 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* 服务调度和负载均衡,两个关键点:摘除故障节点,负载均衡
|
||||
* <p>
|
||||
* 在clientSession中选择一个可用的session,最终还是调用的IRouter中的方法
|
||||
* Service dispatching and load balancing.
|
||||
* Two key concerns: removing faulty nodes and balancing load.
|
||||
* Selects an available session from the client session pool, then delegates to IRouter methods.
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
@@ -99,11 +99,11 @@ public class Consumer implements IConsumer {
|
||||
return list;
|
||||
}
|
||||
|
||||
// Select a consumer loadBalancer
|
||||
// Select a consumer load balancer
|
||||
@Override
|
||||
public IConsumerLoadBalancer selectLoadBalancer(List<Session> providers, Object packet) {
|
||||
// select first consumer loadBalancer
|
||||
// 不同的服务提供者可能会提供同一个接口,消费者可能同时消费了这些提供了同一个接口的服务提供者,取第一个消费者的loadBalancer
|
||||
// Different providers may expose the same interface; if a consumer connects to multiple such providers,
|
||||
// pick the load balancer of the first matching consumer.
|
||||
IConsumerLoadBalancer loadBalancer = null;
|
||||
for (var providerSession : providers) {
|
||||
for (var provider : providerSession.getConsumerRegister().getProviderConfig().getProviders()) {
|
||||
@@ -139,7 +139,7 @@ public class Consumer implements IConsumer {
|
||||
var loadBalancer = selectLoadBalancer(providers, packet);
|
||||
var session = loadBalancer.selectProvider(providers, packet, argument);
|
||||
|
||||
// 下面的代码逻辑同Router的syncAsk,如果修改的话,记得一起修改
|
||||
// The logic below mirrors Router#syncAsk; update both if modified.
|
||||
var clientSignalAttachment = new SignalAttachment();
|
||||
var taskExecutorHash = TaskBus.calTaskExecutorHash(argument);
|
||||
clientSignalAttachment.setTaskExecutorHash(taskExecutorHash);
|
||||
@@ -147,7 +147,7 @@ public class Consumer implements IConsumer {
|
||||
try {
|
||||
SignalBridge.addSignalAttachment(clientSignalAttachment);
|
||||
|
||||
// load balancer之前调用
|
||||
// Invoke before load-balancer hook
|
||||
loadBalancer.beforeLoadBalancer(session, packet, clientSignalAttachment);
|
||||
|
||||
NetContext.getRouter().send(session, packet, clientSignalAttachment);
|
||||
@@ -163,7 +163,7 @@ public class Consumer implements IConsumer {
|
||||
@SuppressWarnings("unchecked")
|
||||
var syncAnswer = new SyncAnswer<>((T) responsePacket, clientSignalAttachment);
|
||||
|
||||
// load balancer之后调用
|
||||
// Invoke after load-balancer hook
|
||||
loadBalancer.afterLoadBalancer(session, packet, clientSignalAttachment);
|
||||
return syncAnswer;
|
||||
} catch (TimeoutException e) {
|
||||
@@ -181,10 +181,10 @@ public class Consumer implements IConsumer {
|
||||
|
||||
var asyncAnswer = NetContext.getRouter().asyncAsk(session, packet, answerClass, argument);
|
||||
|
||||
// load balancer之前调用
|
||||
// Invoke before load-balancer hook
|
||||
loadBalancer.beforeLoadBalancer(session, packet, asyncAnswer.getSignalAttachment());
|
||||
|
||||
// load balancer之后调用
|
||||
// Invoke after load-balancer hook
|
||||
asyncAnswer.thenAccept(responsePacket -> loadBalancer.afterLoadBalancer(session, packet, asyncAnswer.getSignalAttachment()));
|
||||
return asyncAnswer;
|
||||
}
|
||||
|
||||
@@ -33,12 +33,14 @@ public interface IConsumer {
|
||||
IConsumerLoadBalancer selectLoadBalancer(List<Session> providers, Object packet);
|
||||
|
||||
/**
|
||||
* 直接发送,不需要任何返回值
|
||||
* Send a packet without waiting for a response.
|
||||
* <p>
|
||||
* 例子:参考 com.zfoo.app.zapp.chat.controller。FrinedController 的 atApplyFriendRequest方法,客户端发起申请请求,chat服务处理后,再把消息直接发给网关
|
||||
* Example: see FriendController#atApplyFriendRequest in com.zfoo.app.zapp.chat.controller.
|
||||
* The client sends a friend-request, the chat service processes it, then fires the message directly to the gateway.
|
||||
*
|
||||
* @param packet 需要发送的包
|
||||
* @param argument 计算负载均衡的参数,比如用户的id,会使用这个参数计算负载到哪个服务提供者;
|
||||
* @param packet the packet to send
|
||||
* @param argument the parameter used for load-balancing calculation (e.g., user ID),
|
||||
* which determines which service provider to route to
|
||||
*/
|
||||
void send(Object packet, @Nullable Object argument);
|
||||
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ import com.zfoo.scheduler.util.TimeUtils;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 记忆化一致性hash
|
||||
* Memoized consistent hashing — caches the hash ring result for repeated identical inputs.
|
||||
*
|
||||
* @author qmr
|
||||
*/
|
||||
@@ -20,7 +20,7 @@ public class CachedConsistentHashLoadBalancer extends AbstractConsumerLoadBalanc
|
||||
private static final int MAX_CACHE_SIZE = 10_0000;
|
||||
|
||||
/**
|
||||
* cache the sid after load balancer,key:[argument + protocolModuleId] -> value:[sid]
|
||||
* cache the sid after load balancer, key:[argument + protocolModuleId] -> value:[sid]
|
||||
*/
|
||||
private LazyCache<Long, Long> cache = new LazyCache<>(MAX_CACHE_SIZE, EXPIRED_ACCESS_DURATION, 5 * TimeUtils.MILLIS_PER_MINUTE, null);
|
||||
|
||||
|
||||
@@ -31,9 +31,8 @@ import java.util.TreeMap;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
|
||||
/**
|
||||
* 一致性hash负载均衡器,同一个session总是发到同一提供者
|
||||
* <p>
|
||||
* 通过argument计算一致性hash
|
||||
* Consistent-hash load balancer: the same argument always routes to the same service provider.
|
||||
* The consistent hash is computed from the argument's toString() value.
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
@@ -43,7 +42,7 @@ public class ConsistentHashLoadBalancer extends AbstractConsumerLoadBalancer {
|
||||
|
||||
public static final ConsistentHashLoadBalancer INSTANCE = new ConsistentHashLoadBalancer();
|
||||
|
||||
// 数组下标为ProtocolModule的id
|
||||
// Array indexed by ProtocolModule ID
|
||||
private static final AtomicReferenceArray<ConsistentCache> consistentHashMap = new AtomicReferenceArray<>(ProtocolManager.MAX_MODULE_NUM);
|
||||
private static final int VIRTUAL_NODE_NUMS = 200;
|
||||
|
||||
@@ -65,11 +64,11 @@ public class ConsistentHashLoadBalancer extends AbstractConsumerLoadBalancer {
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过argument的toString计算一致性hash,所以传入的argument一般要能代表唯一性,比如用户的id
|
||||
* Compute the consistent hash from argument.toString(), so the argument should be unique (e.g., a user ID).
|
||||
*
|
||||
* @param packet 请求包
|
||||
* @param argument 参数,一般要能代表唯一性,比如用户的id
|
||||
* @return 调用的session
|
||||
* @param packet the request packet
|
||||
* @param argument a unique parameter such as a user ID
|
||||
* @return the selected provider session
|
||||
*/
|
||||
@Override
|
||||
public Session selectProvider(List<Session> providers, Object packet, Object argument) {
|
||||
@@ -84,7 +83,7 @@ public class ConsistentHashLoadBalancer extends AbstractConsumerLoadBalancer {
|
||||
consistentCache = updateModuleToConsistentHash(providers, module);
|
||||
}
|
||||
var providerSids = consistentCache.providerSids;
|
||||
// providers和consistentHashMap的服务提供者不一致同样进行更新操作
|
||||
// Update if the provider list has changed since the cache was built
|
||||
if (providerSids.size() != providers.size() || providers.stream().anyMatch(it -> !providerSids.contains(it.getSid()))) {
|
||||
consistentCache = updateModuleToConsistentHash(providers, module);
|
||||
}
|
||||
@@ -94,7 +93,7 @@ public class ConsistentHashLoadBalancer extends AbstractConsumerLoadBalancer {
|
||||
throw new RunException("no service provides the [module:{}]", module);
|
||||
}
|
||||
var sid = treeMap.getByIndex(nearestIndex);
|
||||
// 因为每次都会对比sid,一定会从providers获得session
|
||||
// Since we compare sid every time, the session is guaranteed to be found in providers
|
||||
return providers.stream().filter(it -> it.getSid() == sid).findFirst().get();
|
||||
}
|
||||
|
||||
@@ -113,10 +112,10 @@ public class ConsistentHashLoadBalancer extends AbstractConsumerLoadBalancer {
|
||||
virtualTreeMap.put(entry.getKey(), entry.getValue().getValue());
|
||||
}
|
||||
|
||||
// 缓存服务提供者的sid
|
||||
// Cache the provider sids
|
||||
var sidSet = new HashSetLong(16);
|
||||
providers.forEach(it -> sidSet.add(it.getSid()));
|
||||
// 使用更高性能的tree map
|
||||
// Use a higher-performance tree map
|
||||
var fastTreeMap = new FastTreeMapIntLong(virtualTreeMap);
|
||||
|
||||
var consistentCache = new ConsistentCache(sidSet, fastTreeMap);
|
||||
|
||||
@@ -25,12 +25,13 @@ import java.util.List;
|
||||
public interface IConsumerLoadBalancer {
|
||||
|
||||
/**
|
||||
* Select a service provider that can provide interface/packet services
|
||||
* 只有一致性hash会使用这个argument参数,如果在一致性hash没有传入argument默认使用随机负载均衡
|
||||
* Select a service provider that can handle the given packet.
|
||||
* Only the consistent-hash balancer uses the {@code argument} parameter;
|
||||
* if no argument is passed, it falls back to random load balancing.
|
||||
*
|
||||
* @param packet 请求包
|
||||
* @param argument 计算参数
|
||||
* @return 一个服务提供者的session
|
||||
* @param packet the request packet
|
||||
* @param argument the parameter used to compute the consistent hash
|
||||
* @return a session of the selected service provider
|
||||
*/
|
||||
Session selectProvider(List<Session> providers, Object packet, @Nullable Object argument);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import com.zfoo.protocol.util.RandomUtils;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 随机负载均衡器,任选服务提供者的其中之一
|
||||
* A random load balancer that selects one of the available service providers at random.
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
|
||||
@@ -45,17 +45,16 @@ public interface IRegistry {
|
||||
String rootPath();
|
||||
|
||||
/**
|
||||
* EN: Get registration information for all service providers
|
||||
* CN: 获取所有服务提供者的注册信息
|
||||
* Get registration information for all remote service providers.
|
||||
*/
|
||||
List<Register> remoteProviderRegisters();
|
||||
|
||||
/**
|
||||
* 监听path路径下的更新
|
||||
* Listen for changes under the given path.
|
||||
*
|
||||
* @param listenerPath 需要监听的路径
|
||||
* @param updateCallback 回调方法,第一个参数是路径,第二个是变化的内容
|
||||
* @param removeCallback 回调方法,第一个参数是路径,第二个是变化的内容
|
||||
* @param listenerPath the path to watch
|
||||
* @param updateCallback callback invoked on data creation/update; first parameter is the path, second is the changed content
|
||||
* @param removeCallback callback invoked on data removal; first parameter is the path
|
||||
*/
|
||||
void addListener(String listenerPath, @Nullable BiConsumer<String, byte[]> updateCallback, @Nullable Consumer<String> removeCallback);
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import com.zfoo.net.config.model.ConsumerModule;
|
||||
import com.zfoo.net.config.model.ProviderConfig;
|
||||
import com.zfoo.net.config.model.ProviderModule;
|
||||
import com.zfoo.protocol.collection.CollectionUtils;
|
||||
import com.zfoo.protocol.exception.ExceptionUtils;
|
||||
import com.zfoo.protocol.util.StringUtils;
|
||||
import com.zfoo.protocol.util.UuidUtils;
|
||||
import org.slf4j.Logger;
|
||||
@@ -40,9 +39,9 @@ public class Register {
|
||||
|
||||
private String id;
|
||||
|
||||
// 服务提供者配置
|
||||
// Service provider configuration
|
||||
private ProviderConfig providerConfig;
|
||||
// 服务消费者配置
|
||||
// Service consumer configuration
|
||||
private ConsumerConfig consumerConfig;
|
||||
|
||||
public static boolean providerHasConsumer(Register providerRegister, Register consumerRegister) {
|
||||
@@ -125,17 +124,17 @@ public class Register {
|
||||
public String toProviderString() {
|
||||
var builder = new StringBuilder();
|
||||
|
||||
// 模块模块名
|
||||
// Module name / node ID
|
||||
builder.append(id);
|
||||
|
||||
// 服务提供者相关配置信息
|
||||
// Provider-related configuration
|
||||
if (Objects.nonNull(providerConfig)) {
|
||||
var providerAddress = providerConfig.getAddress();
|
||||
if (StringUtils.isBlank(providerAddress)) {
|
||||
throw new RuntimeException(StringUtils.format("The address of provider Config cannot be empty"));
|
||||
}
|
||||
builder.append(StringUtils.SPACE).append(StringUtils.VERTICAL_BAR).append(StringUtils.SPACE);
|
||||
// 服务提供者地址
|
||||
// Provider address
|
||||
builder.append(providerAddress);
|
||||
|
||||
builder.append(StringUtils.SPACE).append(StringUtils.VERTICAL_BAR).append(StringUtils.SPACE);
|
||||
@@ -143,12 +142,12 @@ public class Register {
|
||||
.map(it -> StringUtils.joinWith(StringUtils.HYPHEN, it.getProtocolModule(), it.getProvider()))
|
||||
.toList();
|
||||
|
||||
// 服务提供者模块信息列表
|
||||
// List of provider module descriptors
|
||||
builder.append(StringUtils.format("provider:[{}]"
|
||||
, StringUtils.joinWith(StringUtils.COMMA + StringUtils.SPACE, providerModules.toArray())));
|
||||
}
|
||||
|
||||
// 服务消费者相关信息
|
||||
// Consumer-related configuration
|
||||
if (Objects.nonNull(consumerConfig)) {
|
||||
builder.append(StringUtils.SPACE).append(StringUtils.VERTICAL_BAR).append(StringUtils.SPACE);
|
||||
|
||||
@@ -156,7 +155,7 @@ public class Register {
|
||||
.map(it -> StringUtils.joinWith(StringUtils.HYPHEN, it.getLoadBalancer(), it.getConsumer()))
|
||||
.toList();
|
||||
|
||||
// 服务消费者模块信息列表
|
||||
// List of consumer module descriptors
|
||||
builder.append(StringUtils.format("consumer:[{}]"
|
||||
, StringUtils.joinWith(StringUtils.COMMA + StringUtils.SPACE, consumerModules.toArray())));
|
||||
}
|
||||
@@ -167,17 +166,17 @@ public class Register {
|
||||
public String toProviderSimple() {
|
||||
var builder = new StringBuilder();
|
||||
|
||||
// 模块模块名
|
||||
// Module name / node ID
|
||||
builder.append(id);
|
||||
|
||||
// 服务提供者相关配置信息
|
||||
// Provider-related configuration (address only)
|
||||
if (Objects.nonNull(providerConfig)) {
|
||||
var providerAddress = providerConfig.getAddress();
|
||||
if (StringUtils.isBlank(providerAddress)) {
|
||||
throw new RuntimeException(StringUtils.format("The address of provider Config cannot be empty"));
|
||||
}
|
||||
builder.append(StringUtils.SPACE).append(StringUtils.VERTICAL_BAR).append(StringUtils.SPACE);
|
||||
// 服务提供者地址
|
||||
// Provider address
|
||||
builder.append(providerAddress);
|
||||
}
|
||||
return builder.toString();
|
||||
|
||||
@@ -59,7 +59,7 @@ import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 服务注册,服务发现
|
||||
* Service registration and discovery via ZooKeeper.
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
@@ -95,27 +95,27 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
|
||||
private CuratorFramework curator;
|
||||
/**
|
||||
* provider的监听
|
||||
* Curator cache for watching provider nodes.
|
||||
*/
|
||||
private CuratorCache providerCuratorCache;
|
||||
/**
|
||||
* 本地consumer需要消费的provider集合
|
||||
* The set of providers that the local consumer needs to connect to.
|
||||
*/
|
||||
private final Set<Register> providerRegisterSet = new ConcurrentHashSet<>();
|
||||
/**
|
||||
* addListener中的cache全部会被添加到这个集合中,这个集合不包括providerCuratorCache
|
||||
* All CuratorCache objects added via addListener(); does not include providerCuratorCache.
|
||||
*/
|
||||
private final List<CuratorCache> listenerList = new ConcurrentArrayList<>();
|
||||
|
||||
/**
|
||||
* 这个方法简单说就是分3大步骤:
|
||||
* 如果配置中配置的有自己是服务提供者,那么自己作为服务提供者先启动 // 此时尚未注册到zk中
|
||||
* 再启动zk // 干2件事:1.自己是服务提供者,将会注册到zk上 2.自己是服务消费者,会尝试创建TcpClient连接所有的自己要连接的provider
|
||||
* 监听服务提供者节点的变更 // 自己作为消费者,服务提供者的变更会影响到自己
|
||||
* This method can be summarized in 3 steps:
|
||||
* 1. If the local node is configured as a service provider, start it (not yet registered in ZK).
|
||||
* 2. Start the Curator framework: register this node in ZK if it is a provider; connect to relevant providers if it is a consumer.
|
||||
* 3. Watch for changes to provider nodes (since this node acts as a consumer, provider changes affect it).
|
||||
* <p>
|
||||
* 总结:这个注册中心模块最终的结果我认为是,影响到了
|
||||
* ClientSessionMap // 自己作为消费者去连接服务提供者连上后,存的session
|
||||
* ServerSessionMap // 自己作为服务器角色,很多客户端连接自己,连上来后,保存到这
|
||||
* Summary: the registry module ultimately affects:
|
||||
* - ClientSessionMap: sessions established by this consumer when connecting to providers.
|
||||
* - ServerSessionMap: sessions of clients that connect to this node as a server.
|
||||
*/
|
||||
|
||||
private String rootPath = "/zfoo";
|
||||
@@ -136,28 +136,27 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
consumerRootPath = rootPath + "/consumer";
|
||||
}
|
||||
|
||||
// 先启动本地服务提供者(服务提供者仅仅是一个TcpServer)
|
||||
// Start the local service provider first (a provider is simply a TcpServer)
|
||||
startProvider();
|
||||
|
||||
// 再启动curator框架 1.如果自己是服务提供者,将会注册自己到zk上 2.如果是服务消费者,会创建连接到自己关心的服务提供者上
|
||||
// Start the Curator framework: 1. register this node as a provider in ZK; 2. connect to relevant providers as a consumer
|
||||
startCurator();
|
||||
|
||||
// 检测服务提供者的增加或者减少。因为自己作为消费者的话,会影响到自己
|
||||
// Watch for provider node additions or removals, as they affect this node acting as a consumer
|
||||
startProviderCache();
|
||||
}
|
||||
|
||||
private void startProvider() {
|
||||
// 这个ProviderConfig包含了这个进程服务提供者信息
|
||||
// ProviderConfig contains the provider information for this process
|
||||
var providerConfig = NetContext.getConfigManager().getLocalConfig().getProvider();
|
||||
|
||||
// 这句意思是:提供了注册中心的配置(zk),但是却没有服务提供者
|
||||
// Registry (ZK) is configured but no local service provider is declared
|
||||
if (Objects.isNull(providerConfig)) {
|
||||
logger.info("Distributed startup with no providers");
|
||||
return;
|
||||
}
|
||||
|
||||
// 服务提供者也仅仅是一个TcpServer
|
||||
// 这里可以看出并没有指定接口,是找一个可用的端口
|
||||
// A service provider is simply a TcpServer; no specific port is mandated — an available port is used
|
||||
var providerHostAndPort = providerConfig.localHostAndPortOrDefault();
|
||||
var providerServer = new TcpServer(providerHostAndPort);
|
||||
providerServer.start();
|
||||
@@ -165,17 +164,18 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动curator框架,并且在zk客户端连接上zk服务器时:保证创建好/zfoo /provider /consumer 3个“持久化”节点
|
||||
* Start the Curator framework. On successful connection, ensure the three persistent ZK nodes
|
||||
* (/zfoo, /zfoo/provider, /zfoo/consumer) exist.
|
||||
*/
|
||||
private void startCurator() {
|
||||
var registryConfig = NetContext.getConfigManager().getLocalConfig().getRegistry();
|
||||
|
||||
if (!registryConfig.getCenter().toLowerCase().matches("zookeeper")) {
|
||||
throw new IllegalArgumentException(StringUtils
|
||||
.format("[center:{}]注册中心只能是zookeeper", JsonUtils.object2String(registryConfig)));
|
||||
.format("[center:{}] only zookeeper is supported as the registry center", JsonUtils.object2String(registryConfig)));
|
||||
}
|
||||
|
||||
// 读取zk的配置,连接zk服务器
|
||||
// Read ZK configuration and connect to the ZooKeeper server
|
||||
var zookeeperConnectStr = HostAndPort.toHostAndPortListStr(HostAndPort.toHostAndPortList(registryConfig.getAddress().values()));
|
||||
var builder = CuratorFrameworkFactory.builder();
|
||||
builder.connectString(zookeeperConnectStr);
|
||||
@@ -191,21 +191,21 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
@Override
|
||||
public void stateChanged(CuratorFramework client, ConnectionState state) {
|
||||
switch (state) {
|
||||
// zk客户端与zk服务器失去了连接(忽略此情况,使用本地配置的缓存)
|
||||
// ZK client lost connection to the server (use local cached config)
|
||||
case LOST:
|
||||
logger.error("[zookeeper:{}] lost connection, cache used", zookeeperConnectStr);
|
||||
break;
|
||||
|
||||
// 暂停和只读这2种状态不检测
|
||||
// SUSPENDED and READ_ONLY states are not monitored
|
||||
case SUSPENDED:
|
||||
case READ_ONLY:
|
||||
logger.warn("[zookeeper:{}] ignored [state{}]", zookeeperConnectStr, state);
|
||||
break;
|
||||
|
||||
// zk客户端和zk服务器重连了
|
||||
// ZK client reconnected to the server
|
||||
case CONNECTED:
|
||||
case RECONNECTED:
|
||||
// 检查3个持久化节点,不存在就创建
|
||||
// Ensure the three persistent root nodes exist; create them if missing
|
||||
createZookeeperRootPath();
|
||||
break;
|
||||
|
||||
@@ -224,42 +224,42 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 /zfoo /zfoo/provider /zfoo/consumer 这3个“持久化”节点,不存在就创建
|
||||
* Check whether the three persistent ZK nodes (/zfoo, /zfoo/provider, /zfoo/consumer) exist;
|
||||
* create them if they are missing.
|
||||
*/
|
||||
private void createZookeeperRootPath() {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
// /zfoo
|
||||
// 创建zookeeper的根路径
|
||||
// Check and optionally create the ZooKeeper root path
|
||||
var rootStat = curator.checkExists().forPath(rootPath);
|
||||
// 根节点不存在
|
||||
// Root node does not exist
|
||||
if (Objects.isNull(rootStat)) {
|
||||
var registryConfig = NetContext.getConfigManager().getLocalConfig().getRegistry();
|
||||
var builder = curator.create();
|
||||
builder.creatingParentsIfNeeded();
|
||||
// 检查zk连接授权
|
||||
// Verify ZK connection authorisation
|
||||
if (registryConfig.hasZookeeperAuthor()) {
|
||||
var zookeeperAuthorStr = registryConfig.toZookeeperAuthor();
|
||||
var aclList = List.of(new ACL(ZooDefs.Perms.ALL, new Id("digest", DigestAuthenticationProvider.generateDigest(zookeeperAuthorStr))));
|
||||
builder.withACL(aclList);
|
||||
}
|
||||
// 根节点是持久化节点
|
||||
// The root node is a persistent node
|
||||
builder.withMode(CreateMode.PERSISTENT);
|
||||
// 真正创建根节点
|
||||
// Actually create the root node
|
||||
builder.forPath(rootPath, StringUtils.bytes(registryConfig.getCenter()));
|
||||
} else {
|
||||
var registryConfig = NetContext.getConfigManager().getLocalConfig().getRegistry();
|
||||
// 读取根节点上的数据
|
||||
// Read data stored at the root node
|
||||
var bytes = curator.getData().storingStatIn(new Stat()).forPath(rootPath);
|
||||
// 把根节点数据从二进制转string字符串
|
||||
// Convert root node data from bytes to a string
|
||||
var rootPathData = StringUtils.bytesToString(bytes);
|
||||
|
||||
// 检查zookeeper根节点的内容
|
||||
// Verify the root node content matches the configured center
|
||||
if (!rootPathData.equals(registryConfig.getCenter())) {
|
||||
throw new RuntimeException(StringUtils.format("zookeeper rootPath[{}] misconfigured [{}],expected [{}], check the relevant nodes and restart", rootPath, rootPathData, registryConfig.getCenter()));
|
||||
throw new RuntimeException(StringUtils.format("zookeeper rootPath[{}] misconfigured [{}], expected [{}], check the relevant nodes and restart", rootPath, rootPathData, registryConfig.getCenter()));
|
||||
}
|
||||
|
||||
// 检查zookeeper根节点的权限
|
||||
// Verify the ZooKeeper root node ACL permissions
|
||||
if (registryConfig.hasZookeeperAuthor()) {
|
||||
try {
|
||||
var providerRootPathAclList = curator.getACL().forPath(rootPath);
|
||||
@@ -275,8 +275,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
|
||||
}
|
||||
|
||||
// /zfoo/provider
|
||||
// 检查服务提供者节点,不存在则创建
|
||||
// /zfoo/provider — check the provider node; create it if missing
|
||||
var providerStat = curator.checkExists().forPath(providerRootPath);
|
||||
if (Objects.isNull(providerStat)) {
|
||||
curator.create()
|
||||
@@ -284,8 +283,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
.forPath(providerRootPath, ArrayUtils.EMPTY_BYTE_ARRAY);
|
||||
}
|
||||
|
||||
// /zfoo/consumer
|
||||
// 检查消费者节点,不存在则创建
|
||||
// /zfoo/consumer — check the consumer node; create it if missing
|
||||
var consumerStat = curator.checkExists().forPath(consumerRootPath);
|
||||
if (Objects.isNull(consumerStat)) {
|
||||
curator.create()
|
||||
@@ -293,8 +291,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
.forPath(consumerRootPath, ArrayUtils.EMPTY_BYTE_ARRAY);
|
||||
}
|
||||
|
||||
// 如果自己是服务提供者,则注册自己
|
||||
// 如果自己是消费者,则创建连接到所有的自己关心的服务提供者
|
||||
// Register this node as a provider, or connect to all relevant providers as a consumer
|
||||
initZookeeper();
|
||||
} catch (Throwable t) {
|
||||
logger.error("Zookeeper failed to create zookeeper root path, wait [{}] seconds to recreate", RETRY_SECONDS, t);
|
||||
@@ -304,7 +301,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
|
||||
private void startProviderCache() {
|
||||
// 初始化providerCache
|
||||
// Initialize providerCache
|
||||
providerCuratorCache = CuratorCache.builder(curator, providerRootPath)
|
||||
.withExceptionHandler(e -> {
|
||||
logger.error("providerCuratorCache unknown exception", e);
|
||||
@@ -320,12 +317,12 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
logger.error("No need to deal with [oldData:{}] [newData:{}]", childDataToString(oldData), childDataToString(newData));
|
||||
initZookeeper();
|
||||
break;
|
||||
case NODE_CREATED: // 意味着有可能来了自己作为消费者需要关心的服务提供者
|
||||
case NODE_CREATED: // A new provider node may have appeared that this consumer cares about
|
||||
var providerStr = StringUtils.substringAfterFirst(newData.getPath(), providerRootPath + StringUtils.SLASH);
|
||||
var providerRegister = Register.parseString(providerStr);
|
||||
var localRegisterVO = NetContext.getConfigManager().getLocalConfig().toLocalRegister();
|
||||
// 如果启动的Consumer是自己关心的Consumer,那么就会接下来尝试连接他们
|
||||
// 这意味着:如果有多个Consumer启动,那么最后将全部连接上去
|
||||
// If the newly started provider is one this consumer cares about, attempt to connect
|
||||
// This means: if multiple consumers start, they will eventually all connect
|
||||
if (Register.providerHasConsumer(providerRegister, localRegisterVO)) {
|
||||
providerRegisterSet.add(providerRegister);
|
||||
checkConsumer();
|
||||
@@ -357,11 +354,10 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
private void initZookeeper() {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
// 既有Provider,又有Consumer信息,注册到zk上,这是一个临时节点
|
||||
// 这一步是:如果自己是服务提供者,就把自己注册上去
|
||||
// If this node is a provider, register it as an ephemeral node in ZK
|
||||
initLocalProvider();
|
||||
|
||||
// 自己是消费者,则连接所有自己关心的服务提供者
|
||||
// If this node is a consumer, connect to all relevant providers
|
||||
initConsumerCache();
|
||||
} catch (Throwable t) {
|
||||
logger.error("Zookeeper failed to initialize, wait [{}] seconds to reinitialize", RETRY_SECONDS, t);
|
||||
@@ -371,7 +367,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果自己是服务提供者,就把自己注册上去
|
||||
* If this node is a service provider, register it in ZooKeeper as an ephemeral node.
|
||||
*/
|
||||
private void initLocalProvider() throws Exception {
|
||||
var localRegisterVO = NetContext.getConfigManager().getLocalConfig().toLocalRegister();
|
||||
@@ -388,7 +384,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
.forPath(localProviderPath, StringUtils.EMPTY.getBytes());
|
||||
logger.info("Provider register successful at [{}]", localProviderVoStr);
|
||||
} else {
|
||||
// 如果服务提供者已经有节点了,防止这个节点是是上次来不及删除的临时节点
|
||||
// The provider node already exists; guard against stale ephemeral nodes left over from a previous session
|
||||
var curatorSessionId = curator.getZookeeperClient().getZooKeeper().getSessionId();
|
||||
var providerNodeSessionId = localProviderStat.getEphemeralOwner();
|
||||
if (curatorSessionId != providerNodeSessionId) {
|
||||
@@ -405,34 +401,34 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历zk中的provider信息,从而找到所有自己关心的Provider,从而去连接他们
|
||||
* 注意:自己作为Provider那自己启动下就行了比较简单。 但是作为Consumer,那么会尝试连接所有已经注册到zk上来的服务器信息
|
||||
* Iterate over all provider nodes in ZK to find those this node cares about, then connect to them.
|
||||
* Note: acting as a provider only requires starting a TcpServer, which is straightforward.
|
||||
* Acting as a consumer requires connecting to all providers that have already registered in ZK.
|
||||
*/
|
||||
private void initConsumerCache() throws Exception {
|
||||
// /zfoo/provider/applicationNameTest | 192.168.1.104:12400 | provider:[myProviderModule-provider1, myProviderModule-provider2]
|
||||
var localRegisterVO = NetContext.getConfigManager().getLocalConfig().toLocalRegister();
|
||||
// 初始化providerCacheSet
|
||||
// 遍历provider下注册的所有节点
|
||||
// Initialize providerCacheSet by iterating over all registered provider nodes
|
||||
var remoteProviderSet = curator.getChildren().forPath(providerRootPath).stream()
|
||||
.filter(it -> StringUtils.isNotBlank(it) && !"null".equals(it))
|
||||
.map(it -> Register.parseString(it))
|
||||
.filter(it -> Objects.nonNull(it))
|
||||
// 检查是否这个节点是自己关心的节点
|
||||
// Keep only those nodes that this consumer cares about
|
||||
.filter(it -> Register.providerHasConsumer(it, localRegisterVO))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
providerRegisterSet.clear();
|
||||
|
||||
// 将自己关心的节点存起来,接下来,将会开启TcpClient去连接这些Provider,连接上后,将会把这个session保存到ClientSessionMap中
|
||||
// Store the relevant providers; TcpClients will be created to connect to them and sessions saved to ClientSessionMap
|
||||
providerRegisterSet.addAll(remoteProviderSet);
|
||||
|
||||
// 初始化consumer,providerCacheSet改变会导致消费者改变
|
||||
// 如果自己没有连接上远程消费者,则会一直尝试连接
|
||||
// Initialize consumers; changes to providerCacheSet will update the consumer connections
|
||||
// If this node fails to connect to a remote provider, it keeps retrying
|
||||
checkConsumer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 不管是节点增删,都会调用,因为:有可能自己作为消费者,服务提供者增加了也可能减少了,都要检测下
|
||||
* Called whenever a provider node is added or removed, since either event may affect this consumer.
|
||||
*/
|
||||
@Override
|
||||
public void checkConsumer() {
|
||||
@@ -448,7 +444,8 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查下自己作为消费者,需要连接到的Provider是否全部连接上了,没连接上,就会创建TcpClient进行连接
|
||||
* Verify that all providers this consumer needs to connect to are connected.
|
||||
* For any provider not yet connected, create a TcpClient to establish the connection.
|
||||
*/
|
||||
private void doCheckConsumer() {
|
||||
if (curator.getState() != CuratorFrameworkState.STARTED) {
|
||||
@@ -459,7 +456,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
var recheckFlag = false;
|
||||
|
||||
for (var providerCache : providerRegisterSet) {
|
||||
// 先排除已经启动的consumer
|
||||
// Skip providers that already have an active consumer session
|
||||
var consumerClientList = new ArrayList<Session>();
|
||||
NetContext.getSessionManager().forEachClientSession(new Consumer<Session>() {
|
||||
@Override
|
||||
@@ -470,7 +467,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
});
|
||||
|
||||
// consumerClientList大于等于1,说明消费者连接成功了
|
||||
// consumerClientList size >= 1 means the consumer successfully connected
|
||||
if (consumerClientList.size() == 1) {
|
||||
var consumer = consumerClientList.get(0);
|
||||
if (!SessionUtils.isActive(consumer)) {
|
||||
@@ -487,7 +484,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
|
||||
try {
|
||||
// 自己作为消费者,要创建一个TcpClient去连接服务提供者
|
||||
// Acting as a consumer: create a TcpClient to connect to the service provider
|
||||
var client = new TcpClient(HostAndPort.valueOf(providerCache.getProviderConfig().getAddress()));
|
||||
var session = client.start();
|
||||
if (session == null) {
|
||||
@@ -504,7 +501,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
// 将自己的消费者消息写到 /consumer 的临时节点下
|
||||
// Write this consumer's info to an ephemeral node under /consumer
|
||||
updateConsumerData();
|
||||
|
||||
if (recheckFlag) {
|
||||
@@ -530,7 +527,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
return;
|
||||
}
|
||||
|
||||
// 将自己的消费者消息写到 /consumer 的临时节点下
|
||||
// Write this consumer's info to an ephemeral node under /consumer
|
||||
var localRegisterVO = NetContext.getConfigManager().getLocalConfig().toLocalRegister();
|
||||
var path = consumerRootPath + StringUtils.SLASH + localRegisterVO.toConsumerString();
|
||||
addData(path, StringUtils.bytes(JsonUtils.object2String(list)), CreateMode.EPHEMERAL);
|
||||
@@ -556,9 +553,9 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除路径
|
||||
* Delete the node at the given path.
|
||||
*
|
||||
* @param path
|
||||
* @param path the ZooKeeper path to delete
|
||||
*/
|
||||
@Override
|
||||
public void removeData(String path) {
|
||||
@@ -570,10 +567,10 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询某个路径下的数据
|
||||
* Query the data stored at the given ZooKeeper path.
|
||||
*
|
||||
* @param path
|
||||
* @return
|
||||
* @param path the ZooKeeper path to read
|
||||
* @return the raw bytes stored at the path
|
||||
*/
|
||||
@Override
|
||||
public byte[] queryData(String path) {
|
||||
@@ -585,7 +582,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有某个路径
|
||||
* Check whether the given ZooKeeper path exists.
|
||||
*/
|
||||
@Override
|
||||
public boolean haveNode(String path) {
|
||||
@@ -597,7 +594,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询某个路径下的所有子路径
|
||||
* List all immediate child paths under the given ZooKeeper path.
|
||||
*/
|
||||
@Override
|
||||
public List<String> children(String path) {
|
||||
@@ -621,7 +618,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有服务信息(提供者+消费者信息)
|
||||
* Query all registered remote providers.
|
||||
*/
|
||||
@Override
|
||||
public List<Register> remoteProviderRegisters() {
|
||||
@@ -641,13 +638,12 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 某个路径下发生数据变更(更新和创建)
|
||||
* 数据删除
|
||||
* 时,进行回调
|
||||
* Register a listener on the given ZooKeeper path that triggers callbacks on data changes.
|
||||
* The update callback is invoked on node creation or update; the remove callback on deletion.
|
||||
*
|
||||
* @param listenerPath 需要监听的路径
|
||||
* @param updateCallback 回调方法,第一个参数是路径,第二个是变化的内容
|
||||
* @param removeCallback 回调方法,第一个参数是路径,第二个是变化的内容
|
||||
* @param listenerPath the ZooKeeper path to watch
|
||||
* @param updateCallback callback invoked on create/update; first arg is path, second is new data
|
||||
* @param removeCallback callback invoked on deletion; arg is the deleted path
|
||||
*/
|
||||
@Override
|
||||
public void addListener(String listenerPath, BiConsumer<String, byte[]> updateCallback, Consumer<String> removeCallback) {
|
||||
@@ -700,7 +696,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
try {
|
||||
var localRegisterVO = NetContext.getConfigManager().getLocalConfig().toLocalRegister();
|
||||
if (curator.getState() == CuratorFrameworkState.STARTED) {
|
||||
// 删除服务提供者的临时节点
|
||||
// Remove the ephemeral provider node on shutdown
|
||||
if (Objects.nonNull(localRegisterVO.getProviderConfig())) {
|
||||
var localProviderPath = providerRootPath + StringUtils.SLASH + localRegisterVO.toProviderString();
|
||||
var localProviderStat = curator.checkExists().forPath(localProviderPath);
|
||||
@@ -709,7 +705,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
// 删除服务消费者的临时节点
|
||||
// Remove the ephemeral consumer node on shutdown
|
||||
if (Objects.nonNull(localRegisterVO.getConsumerConfig())) {
|
||||
var localConsumerPath = consumerRootPath + StringUtils.SLASH + localRegisterVO.toConsumerString();
|
||||
var localConsumerStat = curator.checkExists().forPath(localConsumerPath);
|
||||
@@ -736,7 +732,7 @@ public class ZookeeperRegistry implements IRegistry {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
|
||||
// 只打印data数据比较小的内容
|
||||
// Only print data inline when the payload is small
|
||||
if (childData.getData() == null || childData.getData().length <= 8) {
|
||||
return childData.toString();
|
||||
}
|
||||
|
||||
@@ -35,17 +35,17 @@ import java.util.List;
|
||||
public abstract class AbstractServer<C extends Channel> extends ChannelInitializer<C> implements IServer {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AbstractServer.class);
|
||||
|
||||
// 所有的服务器都可以在这个列表中取到
|
||||
// Registry of all started server instances
|
||||
protected static final List<AbstractServer<? extends Channel>> allServers = new ArrayList<>(1);
|
||||
|
||||
protected String hostAddress;
|
||||
protected int port;
|
||||
|
||||
|
||||
// 配置服务端nio线程组,服务端接受客户端连接
|
||||
// Boss group: accepts incoming client connections
|
||||
private EventLoopGroup bossGroup;
|
||||
|
||||
// SocketChannel的网络读写
|
||||
// Worker group: handles network I/O for each SocketChannel
|
||||
protected EventLoopGroup workerGroup;
|
||||
|
||||
protected ChannelFuture channelFuture;
|
||||
@@ -62,7 +62,7 @@ public abstract class AbstractServer<C extends Channel> extends ChannelInitializ
|
||||
|
||||
protected synchronized void doStart() {
|
||||
var cpuNum = Runtime.getRuntime().availableProcessors();
|
||||
// 一条线程持有一个端口对应的selector,如果我们启动不仅仅是一个服务器端口的话,为了更好的性能需要修改对应的bossGroup数量
|
||||
// Each boss thread owns one selector per port; increase bossGroup size if multiple server ports are used
|
||||
bossGroup = Epoll.isAvailable()
|
||||
? new EpollEventLoopGroup(Math.max(1, cpuNum / 8), new DefaultThreadFactory("netty-boss", true))
|
||||
: new NioEventLoopGroup(Math.max(1, cpuNum / 8), new DefaultThreadFactory("netty-boss", true));
|
||||
@@ -78,13 +78,12 @@ public abstract class AbstractServer<C extends Channel> extends ChannelInitializ
|
||||
.childOption(ChannelOption.TCP_NODELAY, true)
|
||||
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(16 * IOUtils.BYTES_PER_KB, 16 * IOUtils.BYTES_PER_MB))
|
||||
.childHandler(this);
|
||||
// 绑定端口,同步等待成功
|
||||
// Bind the port synchronously (commented-out alternative):
|
||||
// channelFuture = bootstrap.bind(hostAddress, port).sync();
|
||||
// 等待服务端监听端口关闭
|
||||
// Block until the server port is closed (commented-out alternative):
|
||||
// channelFuture.channel().closeFuture().sync();
|
||||
|
||||
|
||||
// 异步
|
||||
// Async bind — use syncUninterruptibly to wait for bind completion without blocking indefinitely
|
||||
channelFuture = bootstrap.bind(hostAddress, port);
|
||||
channelFuture.syncUninterruptibly();
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ import com.zfoo.protocol.anno.Protocol;
|
||||
@Protocol(id = 150)
|
||||
public class BrokerRegisterAsk {
|
||||
|
||||
// 不带gpu的服务器
|
||||
// Server without GPU
|
||||
public static final int HOME = 1;
|
||||
// 带了gpu的服务器
|
||||
// Server with GPU
|
||||
public static final int HOME_GPU = 2;
|
||||
|
||||
private int brokerType;
|
||||
|
||||
@@ -36,8 +36,8 @@ public class BrokerTcpClient extends AbstractClient<SocketChannel> {
|
||||
|
||||
@Override
|
||||
protected void initChannel(SocketChannel channel) {
|
||||
// 可以看出来,这个客户端检测到空闲的时间是60s,相对短一点,这样子就可以发送心跳。
|
||||
// 服务器端则是180s,相对长一点,一旦检测到空闲,则把客户端踢掉。
|
||||
// The client's idle detection timeout is 60 s (shorter), allowing it to send heartbeats.
|
||||
// The server's idle detection timeout is 180 s (longer); once idle is detected, the client is evicted.
|
||||
channel.pipeline().addLast(new IdleStateHandler(0, 0, 60));
|
||||
channel.pipeline().addLast(new ClientIdleHandler());
|
||||
channel.pipeline().addLast(new TcpCodecHandler());
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
package com.zfoo.net.core.gateway;
|
||||
|
||||
/**
|
||||
* 网关负载均衡使用计算一致性hash的参数,如果packet继承了这个接口,则网关的一致性hash负载均衡优先使用这个接口计算一致性hash;
|
||||
* The parameter used by the gateway load balancer to compute a consistent hash.
|
||||
* If a packet implements this interface, the gateway's consistent-hash load balancer
|
||||
* will use this value as the hash input with highest priority.
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
|
||||
@@ -33,7 +33,7 @@ import java.util.function.Function;
|
||||
public class HttpServer extends AbstractServer<SocketChannel> {
|
||||
|
||||
/**
|
||||
* http的地址解析器
|
||||
* URI resolver that maps an HTTP request to a decoded packet.
|
||||
*/
|
||||
private Function<FullHttpRequest, DecodedPacketInfo> uriResolver;
|
||||
|
||||
|
||||
@@ -39,16 +39,15 @@ public class JsonWebsocketServer extends AbstractServer<SocketChannel> {
|
||||
public void initChannel(SocketChannel channel) {
|
||||
channel.pipeline().addLast(new IdleStateHandler(0, 0, 180));
|
||||
channel.pipeline().addLast(new ServerIdleHandler());
|
||||
// 编解码 http 请求
|
||||
// Encode/decode HTTP requests
|
||||
channel.pipeline().addLast(new HttpServerCodec(8 * IOUtils.BYTES_PER_KB, 16 * IOUtils.BYTES_PER_KB, 16 * IOUtils.BYTES_PER_KB));
|
||||
// 聚合解码 HttpRequest/HttpContent/LastHttpContent 到 FullHttpRequest
|
||||
// 保证接收的 Http 请求的完整性
|
||||
// Aggregate HttpRequest/HttpContent/LastHttpContent into a single FullHttpRequest to ensure completeness
|
||||
channel.pipeline().addLast(new HttpObjectAggregator(16 * IOUtils.BYTES_PER_MB));
|
||||
// 处理其他的 WebSocketFrame
|
||||
// Handle WebSocketFrame upgrades and other WebSocket frames
|
||||
channel.pipeline().addLast(new WebSocketServerProtocolHandler("/websocket"));
|
||||
// 写文件内容,支持异步发送大的码流,一般用于发送文件流
|
||||
// Support async large-payload streaming (e.g. file transfers)
|
||||
channel.pipeline().addLast(new ChunkedWriteHandler());
|
||||
// 编解码WebSocketFrame二进制协议
|
||||
// Encode/decode WebSocketFrame binary protocol
|
||||
channel.pipeline().addLast(new JsonWebSocketCodecHandler());
|
||||
channel.pipeline().addLast(new ServerRouteHandler());
|
||||
}
|
||||
|
||||
@@ -41,19 +41,19 @@ public class ProxyCodecHandler extends ByteToMessageCodec<TunnelProtocolServer2C
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
||||
// 不够读一个int
|
||||
// Not enough bytes to read a single int
|
||||
if (in.readableBytes() <= PacketService.PACKET_HEAD_LENGTH) {
|
||||
return;
|
||||
}
|
||||
in.markReaderIndex();
|
||||
var length = in.readInt();
|
||||
|
||||
// 如果长度非法,则抛出异常断开连接,按照自己的使用场景指定合适的长度,防止客户端发送超大包占用带宽
|
||||
// Reject illegal lengths to prevent oversized packets from exhausting bandwidth
|
||||
if (length < 0 || length > IOUtils.BYTES_PER_MB) {
|
||||
throw new IllegalArgumentException(StringUtils.format("illegal packet [length:{}]", length));
|
||||
}
|
||||
|
||||
// ByteBuf里的数据太小
|
||||
// Not enough data buffered yet in ByteBuf
|
||||
if (in.readableBytes() < length) {
|
||||
in.resetReaderIndex();
|
||||
return;
|
||||
|
||||
@@ -33,19 +33,19 @@ public class TunnelClientCodecHandler extends ByteToMessageCodec<Object> {
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
||||
// 不够读一个int
|
||||
// Not enough bytes to read a single int
|
||||
if (in.readableBytes() <= PacketService.PACKET_HEAD_LENGTH) {
|
||||
return;
|
||||
}
|
||||
in.markReaderIndex();
|
||||
var length = in.readInt();
|
||||
|
||||
// 如果长度非法,则抛出异常断开连接,按照自己的使用场景指定合适的长度,防止客户端发送超大包占用带宽
|
||||
// Reject illegal lengths to prevent oversized packets from exhausting bandwidth
|
||||
if (length < 0 || length > IOUtils.BYTES_PER_MB) {
|
||||
throw new IllegalArgumentException(StringUtils.format("illegal packet [length:{}]", length));
|
||||
}
|
||||
|
||||
// ByteBuf里的数据太小
|
||||
// Not enough data buffered yet in ByteBuf
|
||||
if (in.readableBytes() < length) {
|
||||
in.resetReaderIndex();
|
||||
return;
|
||||
|
||||
@@ -40,19 +40,19 @@ public class TunnelServerCodecHandler extends ByteToMessageCodec<TunnelProtocolS
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
||||
// 不够读一个int
|
||||
// Not enough bytes to read a single int
|
||||
if (in.readableBytes() <= PacketService.PACKET_HEAD_LENGTH) {
|
||||
return;
|
||||
}
|
||||
in.markReaderIndex();
|
||||
var length = in.readInt();
|
||||
|
||||
// 如果长度非法,则抛出异常断开连接,按照自己的使用场景指定合适的长度,防止客户端发送超大包占用带宽
|
||||
// Reject illegal lengths to prevent oversized packets from exhausting bandwidth
|
||||
if (length < 0 || length > IOUtils.BYTES_PER_MB) {
|
||||
throw new IllegalArgumentException(StringUtils.format("illegal packet [length:{}]", length));
|
||||
}
|
||||
|
||||
// ByteBuf里的数据太小
|
||||
// Not enough data buffered yet in ByteBuf
|
||||
if (in.readableBytes() < length) {
|
||||
in.resetReaderIndex();
|
||||
return;
|
||||
|
||||
@@ -31,8 +31,8 @@ public class TcpClient extends AbstractClient<SocketChannel> {
|
||||
|
||||
@Override
|
||||
protected void initChannel(SocketChannel channel) {
|
||||
// 可以看出来,这个客户端检测到空闲的时间是60s,相对短一点,这样子就可以发送心跳。
|
||||
// 服务器端则是180s,相对长一点,一旦检测到空闲,则把客户端踢掉。
|
||||
// The client's idle detection timeout is 60 s (shorter), allowing it to send heartbeats.
|
||||
// The server's idle detection timeout is 180 s (longer); once idle is detected, the client is evicted.
|
||||
channel.pipeline().addLast(new IdleStateHandler(0, 0, 60));
|
||||
channel.pipeline().addLast(new ClientIdleHandler());
|
||||
channel.pipeline().addLast(new TcpCodecHandler());
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
package com.zfoo.net.core.udp;
|
||||
|
||||
import com.zfoo.net.NetContext;
|
||||
import com.zfoo.net.core.AbstractClient;
|
||||
import com.zfoo.net.core.HostAndPort;
|
||||
import com.zfoo.net.handler.BaseRouteHandler;
|
||||
@@ -44,7 +43,7 @@ public class UdpClient extends AbstractClient<Channel> {
|
||||
.option(ChannelOption.SO_BROADCAST, true)
|
||||
.handler(this);
|
||||
|
||||
// bind(0)随机选择一个端口
|
||||
// bind(0) lets the OS pick a random available port
|
||||
var channelFuture = bootstrap.bind(0);
|
||||
channelFuture.syncUninterruptibly();
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ public class UdpServer extends AbstractServer<Channel> {
|
||||
public void start() {
|
||||
var cpuNum = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
// 配置服务端nio线程组
|
||||
// Configure the server-side NIO event loop group
|
||||
workerGroup = Epoll.isAvailable()
|
||||
? new EpollEventLoopGroup(cpuNum * 2, new DefaultThreadFactory("netty-worker", true))
|
||||
: new NioEventLoopGroup(cpuNum * 2, new DefaultThreadFactory("netty-worker", true));
|
||||
@@ -52,7 +52,7 @@ public class UdpServer extends AbstractServer<Channel> {
|
||||
.option(ChannelOption.SO_BROADCAST, true)
|
||||
.handler(this);
|
||||
|
||||
// 异步
|
||||
// Bind asynchronously
|
||||
channelFuture = bootstrap.bind(hostAddress, port);
|
||||
channelFuture.syncUninterruptibly();
|
||||
|
||||
|
||||
@@ -39,16 +39,15 @@ public class WebsocketServer extends AbstractServer<SocketChannel> {
|
||||
public void initChannel(SocketChannel channel) {
|
||||
channel.pipeline().addLast(new IdleStateHandler(0, 0, 180));
|
||||
channel.pipeline().addLast(new ServerIdleHandler());
|
||||
// 编解码 http 请求
|
||||
// Encode/decode HTTP requests
|
||||
channel.pipeline().addLast(new HttpServerCodec(8 * IOUtils.BYTES_PER_KB, 16 * IOUtils.BYTES_PER_KB, 16 * IOUtils.BYTES_PER_KB));
|
||||
// 聚合解码 HttpRequest/HttpContent/LastHttpContent 到 FullHttpRequest
|
||||
// 保证接收的 Http 请求的完整性
|
||||
// Aggregate HttpRequest/HttpContent/LastHttpContent into FullHttpRequest to ensure request completeness
|
||||
channel.pipeline().addLast(new HttpObjectAggregator(16 * IOUtils.BYTES_PER_MB));
|
||||
// 处理其他的 WebSocketFrame
|
||||
// Handle WebSocketFrame messages
|
||||
channel.pipeline().addLast(new WebSocketServerProtocolHandler("/websocket"));
|
||||
// 写文件内容,支持异步发送大的码流,一般用于发送文件流
|
||||
// Write file content; supports async sending of large byte streams (e.g., file transfers)
|
||||
channel.pipeline().addLast(new ChunkedWriteHandler());
|
||||
// 编解码WebSocketFrame二进制协议
|
||||
// Encode/decode WebSocketFrame binary protocol
|
||||
channel.pipeline().addLast(new WebSocketCodecHandler());
|
||||
channel.pipeline().addLast(new ServerRouteHandler());
|
||||
}
|
||||
|
||||
@@ -39,17 +39,17 @@ public class WebsocketSslServer extends AbstractServer<SocketChannel> {
|
||||
private SslContext sslContext;
|
||||
|
||||
/**
|
||||
* 方式一:
|
||||
* 下载的nginx格式的证书,里面包含pem和key两种格式的文件。其中pem格式我们可以直接用,key格式的Java无法直接使用。
|
||||
* 我们使用OpenSSL将PKCS1 key格式证书换一个可以使用的PKCS8 key文件出来:
|
||||
* Option 1: nginx certificate format
|
||||
* Download the nginx-format certificate, which contains a .pem file and a .key file.
|
||||
* The .pem file can be used directly; the .key file (PKCS1) cannot be used by Java as-is.
|
||||
* Convert the PKCS1 key to PKCS8 using OpenSSL:
|
||||
* openssl pkcs8 -topk8 -nocrypt -in ws.jiucai.fun.key -out ws.jiucai.fun.key.pem
|
||||
* <p>
|
||||
* 方式二:阿里云证书转换
|
||||
* 进入阿里云证书下载页面,如下图。要下载两份,一个是tomcat的,一个是nginx的。
|
||||
* 下载的nginx格式的证书,里面包含pem和key两种格式的文件。其中pem格式我们可以直接用,key格式的Java无法直接使用。
|
||||
* 我们使用OpenSSL将前面下载tomcat格式证书pfx文件转换一个可以使用的key文件出来:
|
||||
* Option 2: Alibaba Cloud certificate conversion
|
||||
* Download two certificate bundles from the Alibaba Cloud console: one for Tomcat (.pfx), one for nginx.
|
||||
* Use the nginx bundle's .pem directly, and convert the Tomcat .pfx to a usable key file:
|
||||
* openssl pkcs12 -in ws.jiucai.fun.pfx -nocerts -nodes -out ws.jiucai.fun.key
|
||||
* openssl会让输入密码,密码是tomcat下载文件里的pfx-password.txt
|
||||
* (OpenSSL will prompt for the password, which is stored in pfx-password.txt inside the Tomcat bundle)
|
||||
*/
|
||||
public WebsocketSslServer(HostAndPort host, InputStream pem, InputStream key) {
|
||||
super(host);
|
||||
|
||||
@@ -65,7 +65,7 @@ public abstract class EnhanceUtils {
|
||||
var invokeMethod = new CtMethod(classPool.get(void.class.getName()), "invoke", classPool.get(new String[]{Session.class.getName(), Object.class.getName(), Object.class.getName()}), enhanceClazz);
|
||||
invokeMethod.setModifiers(Modifier.PUBLIC + Modifier.FINAL);
|
||||
if (attachmentClazz == null) {
|
||||
// Cast type(强制类型转换)
|
||||
// Cast types explicitly to match the method signature
|
||||
String invokeMethodBody = StringUtils.format("{this.bean.{}($1, ({})$2);}", method.getName(), packetClazz.getName());
|
||||
invokeMethod.setBody(invokeMethodBody);
|
||||
} else {
|
||||
@@ -74,14 +74,14 @@ public abstract class EnhanceUtils {
|
||||
}
|
||||
enhanceClazz.addMethod(invokeMethod);
|
||||
|
||||
// 定义类实现的接口方法task
|
||||
// Define the 'task' interface method
|
||||
CtMethod taskMethod = new CtMethod(classPool.get(Task.class.getName()), "task", null, enhanceClazz);
|
||||
taskMethod.setModifiers(Modifier.PUBLIC + Modifier.FINAL);
|
||||
String taskMethodBody = StringUtils.format("{ return {}.{}; }", Task.class.getName(), definition.getTask());
|
||||
taskMethod.setBody(taskMethodBody);
|
||||
enhanceClazz.addMethod(taskMethod);
|
||||
|
||||
// 定义类实现的接口方法attachment
|
||||
// Define the 'attachment' interface method
|
||||
CtMethod attachmentMethod = new CtMethod(classPool.get(Class.class.getName()), "attachment", null, enhanceClazz);
|
||||
attachmentMethod.setModifiers(Modifier.PUBLIC + Modifier.FINAL);
|
||||
if (attachmentClazz == null) {
|
||||
|
||||
@@ -20,8 +20,8 @@ import com.zfoo.protocol.util.ReflectionUtils;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* EN:Dynamic proxy methods annotated by PacketReceiver annotations, to avoid reflection, will eventually use javassist bytecode enhanced methods to proxy this class
|
||||
* CN:动态代理被PacketReceiver注解标注的方法,为了避免反射最终会用javassist字节码增强的方法去代理PacketReceiverDefinition
|
||||
* Dynamic proxy for methods annotated with @PacketReceiver.
|
||||
* To avoid reflection overhead, Javassist bytecode enhancement is ultimately used to proxy this class.
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,6 @@ import com.zfoo.event.manager.EventBus;
|
||||
import com.zfoo.net.NetContext;
|
||||
import com.zfoo.net.core.event.ClientSessionActiveEvent;
|
||||
import com.zfoo.net.core.event.ClientSessionInactiveEvent;
|
||||
import com.zfoo.net.core.event.ServerSessionActiveEvent;
|
||||
import com.zfoo.net.util.SessionUtils;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
@@ -54,7 +53,7 @@ public class ClientRouteHandler extends BaseRouteHandler {
|
||||
NetContext.getSessionManager().removeClientSession(session);
|
||||
EventBus.post(ClientSessionInactiveEvent.valueOf(session));
|
||||
|
||||
// 如果是消费者inactive,还需要触发客户端消费者检查事件,以便重新连接
|
||||
// If a consumer session becomes inactive, trigger a consumer-check event to attempt reconnection
|
||||
if (session.getConsumerRegister() != null) {
|
||||
NetContext.getConfigManager().getRegistry().checkConsumer();
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ public class GatewayRouteHandler extends ServerRouteHandler {
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
||||
// 请求者的session,一般是serverSession
|
||||
// Requester's session, typically a serverSession
|
||||
var session = SessionUtils.getSession(ctx);
|
||||
if (session == null) {
|
||||
return;
|
||||
@@ -72,13 +72,13 @@ public class GatewayRouteHandler extends ServerRouteHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// 过滤非法包
|
||||
// Filter illegal packets
|
||||
if (packetFilter.apply(session, packet)) {
|
||||
throw new IllegalArgumentException(StringUtils.format(" session:{}发送了一个非法包[{}]", SessionUtils.sessionSimpleInfo(ctx), JsonUtils.object2String(packet)));
|
||||
throw new IllegalArgumentException(StringUtils.format(" session:{} sent an illegal packet [{}]", SessionUtils.sessionSimpleInfo(ctx), JsonUtils.object2String(packet)));
|
||||
}
|
||||
|
||||
|
||||
// 把客户端信息包装为一个GatewayAttachment,因此通过这个网关附加包可以得到用户或玩家的uid、sid之类的信息
|
||||
// Wrap the client information into a GatewayAttachment,
|
||||
// so that uid/sid and other client info can be retrieved from the attachment downstream.
|
||||
var gatewayAttachment = new GatewayAttachment(session);
|
||||
var signalAttachment = (SignalAttachment) decodedPacketInfo.getAttachment();
|
||||
if (signalAttachment != null) {
|
||||
@@ -86,34 +86,36 @@ public class GatewayRouteHandler extends ServerRouteHandler {
|
||||
gatewayAttachment.setSignalAttachment(signalAttachment);
|
||||
}
|
||||
|
||||
// 网关优先使用IGatewayLoadBalancer作为一致性hash的计算参数,然后才会使用客户端的session做参数
|
||||
// 例子:以聊天服务来说,用户或玩家知道自己在哪个群组groupId中,那往这个群发送消息时,会在Packet中带上这个groupId做为一致性hash就可以了。
|
||||
// The gateway prefers IGatewayLoadBalancer as the consistent-hash parameter;
|
||||
// only falls back to the client session when not implemented.
|
||||
// Example: for a chat service, the client knows its groupId, so it includes groupId in the Packet as the hash key.
|
||||
if (packet instanceof IGatewayLoadBalancer) {
|
||||
var loadBalancerConsistentHashObject = ((IGatewayLoadBalancer) packet).loadBalancerConsistentHashObject();
|
||||
gatewayAttachment.setTaskExecutorHash(loadBalancerConsistentHashObject.hashCode());
|
||||
forwardingPacket(packet, gatewayAttachment, loadBalancerConsistentHashObject);
|
||||
return;
|
||||
} else {
|
||||
// 使用用户的uid做一致性hash
|
||||
// Use the user's uid as the consistent-hash key
|
||||
var uid = session.getUid();
|
||||
if (uid > 0) {
|
||||
forwardingPacket(packet, gatewayAttachment, uid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 再使用session的sid做一致性hash,因为每次客户端连接过来sid都会改变,所以客户端重新建立连接的话可能会被路由到其它的服务器
|
||||
// 如果有特殊需求的话,可以考虑去重写网关的转发策略
|
||||
// 拿着用户或玩家的sid做一致性hash,那肯定是:一旦重连sid就会一直变化。所以:一般情况下除非自己创建TcpClient,否则逻辑不应该走到这里。 而是走上面的通过UID做一致性hash
|
||||
// Fall back to the session's sid as the consistent-hash key.
|
||||
// Note: since sid changes on every reconnect, the client may be routed to a different server after reconnecting.
|
||||
// Override the gateway's forwarding strategy if custom behavior is needed.
|
||||
// In most cases, logic should reach the uid-based routing above, not here.
|
||||
var sid = session.getSid();
|
||||
forwardingPacket(packet, gatewayAttachment, sid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转发网关收到的包到Provider
|
||||
* Forward a packet received by the gateway to the Provider.
|
||||
*/
|
||||
private void forwardingPacket(Object packet, Object attachment, Object argument) {
|
||||
try {
|
||||
// 网关统一用 moduleid uid 获取 session
|
||||
// The gateway uses moduleId + uid to locate the session
|
||||
var providers = NetContext.getConsumer().findProviders(packet);
|
||||
var loadBalancer = NetContext.getConsumer().selectLoadBalancer(providers, packet);
|
||||
var providerSession = loadBalancer.selectProvider(providers, packet, argument);
|
||||
@@ -135,7 +137,7 @@ public class GatewayRouteHandler extends ServerRouteHandler {
|
||||
var sid = session.getSid();
|
||||
var uid = session.getUid();
|
||||
|
||||
// 连接到网关的客户端断开了连接
|
||||
// A client connected to the gateway has disconnected
|
||||
EventBus.post(GatewaySessionInactiveEvent.valueOf(sid, uid));
|
||||
|
||||
super.channelInactive(ctx);
|
||||
|
||||
@@ -34,25 +34,25 @@ public class JsonTcpCodecHandler extends ByteToMessageCodec<EncodedPacketInfo> {
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
||||
// 不够读一个int
|
||||
// Not enough bytes to read a single int
|
||||
if (in.readableBytes() <= PACKET_HEAD_LENGTH) {
|
||||
return;
|
||||
}
|
||||
in.markReaderIndex();
|
||||
var length = in.readInt();
|
||||
|
||||
// 如果长度非法,则抛出异常断开连接,按照自己的使用场景指定合适的长度,防止客户端发送超大包占用带宽
|
||||
// Reject illegal lengths to prevent oversized packets from exhausting bandwidth; tune max size to your use-case
|
||||
if (length < 0 || length > IOUtils.BYTES_PER_MB) {
|
||||
throw new IllegalArgumentException(StringUtils.format("illegal packet [length:{}]", length));
|
||||
}
|
||||
|
||||
// ByteBuf里的数据太小
|
||||
// Not enough data buffered yet in ByteBuf
|
||||
if (in.readableBytes() < length) {
|
||||
in.resetReaderIndex();
|
||||
return;
|
||||
}
|
||||
|
||||
// readSlice和byte[]数组相比,readSlice减少了垃圾回收
|
||||
// Using readSlice avoids copying to a byte array and reduces GC pressure
|
||||
var sliceByteBuf = in.readSlice(length);
|
||||
var packetInfo = JsonPacket.readDecodedPacketInfo(sliceByteBuf);
|
||||
out.add(packetInfo);
|
||||
|
||||
@@ -34,25 +34,25 @@ public class TcpCodecHandler extends ByteToMessageCodec<EncodedPacketInfo> {
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
||||
// 不够读一个int
|
||||
// Not enough bytes to read the length header (4 bytes)
|
||||
if (in.readableBytes() <= PacketService.PACKET_HEAD_LENGTH) {
|
||||
return;
|
||||
}
|
||||
in.markReaderIndex();
|
||||
var length = in.readInt();
|
||||
|
||||
// 如果长度非法,则抛出异常断开连接,按照自己的使用场景指定合适的长度,防止客户端发送超大包占用带宽
|
||||
// Reject illegal packet lengths to prevent oversized packets from consuming bandwidth
|
||||
if (length < 0 || length > IOUtils.BYTES_PER_MB) {
|
||||
throw new IllegalArgumentException(StringUtils.format("illegal packet [length:{}]", length));
|
||||
}
|
||||
|
||||
// ByteBuf里的数据太小
|
||||
// Not enough data in ByteBuf yet
|
||||
if (in.readableBytes() < length) {
|
||||
in.resetReaderIndex();
|
||||
return;
|
||||
}
|
||||
|
||||
// readSlice和byte[]数组相比,readSlice减少了垃圾回收
|
||||
// readSlice avoids array copying, reducing GC pressure compared to byte[]
|
||||
var sliceByteBuf = in.readSlice(length);
|
||||
var packetInfo = NetContext.getPacketService().read(sliceByteBuf);
|
||||
out.add(packetInfo);
|
||||
|
||||
@@ -35,7 +35,7 @@ public class UdpCodecHandler extends MessageToMessageCodec<DatagramPacket, Encod
|
||||
protected void decode(ChannelHandlerContext channelHandlerContext, DatagramPacket datagramPacket, List<Object> list) {
|
||||
ByteBuf in = datagramPacket.content();
|
||||
|
||||
// 不够读一个int
|
||||
// Not enough bytes to read the length header (4 bytes)
|
||||
if (in.readableBytes() <= PacketService.PACKET_HEAD_LENGTH) {
|
||||
return;
|
||||
}
|
||||
@@ -43,12 +43,12 @@ public class UdpCodecHandler extends MessageToMessageCodec<DatagramPacket, Encod
|
||||
in.markReaderIndex();
|
||||
var length = in.readInt();
|
||||
|
||||
// 如果长度非法,则抛出异常断开连接,按照自己的使用场景指定合适的长度,防止客户端发送超大包占用带宽
|
||||
// Reject illegal packet lengths to prevent oversized packets from consuming bandwidth
|
||||
if (length < 0 || length > IOUtils.BYTES_PER_MB) {
|
||||
throw new IllegalArgumentException(StringUtils.format("illegal packet [length:{}]", length));
|
||||
}
|
||||
|
||||
// ByteBuf里的数据太小
|
||||
// Not enough data in ByteBuf yet
|
||||
if (in.readableBytes() < length) {
|
||||
in.resetReaderIndex();
|
||||
return;
|
||||
|
||||
@@ -37,7 +37,7 @@ public class WebSocketCodecHandler extends MessageToMessageCodec<WebSocketFrame,
|
||||
protected void decode(ChannelHandlerContext channelHandlerContext, WebSocketFrame webSocketFrame, List<Object> list) {
|
||||
ByteBuf in = webSocketFrame.content();
|
||||
var length = in.readInt();
|
||||
// 如果长度非法,则抛出异常断开连接,按照自己的使用场景指定合适的长度,防止客户端发送超大包占用带宽
|
||||
// Reject illegal packet lengths to prevent oversized packets from consuming bandwidth
|
||||
if (length < 0 || length > IOUtils.BYTES_PER_MB) {
|
||||
throw new IllegalArgumentException(StringUtils.format("illegal packet [length:{}]", length));
|
||||
}
|
||||
|
||||
@@ -19,12 +19,12 @@ package com.zfoo.net.packet;
|
||||
public class DecodedPacketInfo {
|
||||
|
||||
/**
|
||||
* 解码后的包
|
||||
* The decoded packet.
|
||||
*/
|
||||
private Object packet;
|
||||
|
||||
/**
|
||||
* 解码后的包的附加包
|
||||
* The attachment of the decoded packet.
|
||||
*/
|
||||
private Object attachment;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ package com.zfoo.net.packet;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* 被解码后的Packet的信息
|
||||
* Information about an encoded (outbound) packet.
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
@@ -25,12 +25,12 @@ public class EncodedPacketInfo {
|
||||
private long uid;
|
||||
|
||||
/**
|
||||
* 解码后的包
|
||||
* The packet to be sent.
|
||||
*/
|
||||
private Object packet;
|
||||
|
||||
/**
|
||||
* 解码后的包的附加包
|
||||
* The attachment associated with the packet.
|
||||
*/
|
||||
private Object attachment;
|
||||
|
||||
|
||||
@@ -13,14 +13,12 @@
|
||||
package com.zfoo.net.packet;
|
||||
|
||||
import com.zfoo.net.NetContext;
|
||||
import com.zfoo.net.router.attachment.SignalAttachment;
|
||||
import com.zfoo.protocol.ProtocolManager;
|
||||
import com.zfoo.protocol.buffer.ByteBufUtils;
|
||||
import com.zfoo.protocol.collection.CollectionUtils;
|
||||
import com.zfoo.protocol.exception.ExceptionUtils;
|
||||
import com.zfoo.protocol.generate.GenerateOperation;
|
||||
import com.zfoo.protocol.generate.GenerateProtocolFile;
|
||||
import com.zfoo.protocol.registration.IProtocolRegistration;
|
||||
import com.zfoo.protocol.serializer.CodeLanguage;
|
||||
import com.zfoo.protocol.util.DomUtils;
|
||||
import com.zfoo.protocol.util.NumberUtils;
|
||||
@@ -36,7 +34,6 @@ import org.springframework.util.ResourceUtils;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* @author godotg
|
||||
@@ -46,7 +43,7 @@ public class PacketService implements IPacketService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(PacketService.class);
|
||||
|
||||
/**
|
||||
* 包体的头部的长度,一个int字节长度
|
||||
* Length of the packet header, equal to the size of one int (4 bytes).
|
||||
*/
|
||||
public static final int PACKET_HEAD_LENGTH = 4;
|
||||
|
||||
@@ -76,10 +73,10 @@ public class PacketService implements IPacketService {
|
||||
}
|
||||
generateOperation.getGenerateLanguages().addAll(codeLanguageSet);
|
||||
}
|
||||
// 设置生成协议的过滤器
|
||||
// Set the protocol generation filter
|
||||
GenerateProtocolFile.generateProtocolFilter = GenerateProtocolFile.DefaultNetGenerateProtocolFilter;
|
||||
|
||||
// 解析protocol.xml文件,并将协议生成ProtocolRegistration
|
||||
// Parse protocol.xml and generate ProtocolRegistration objects
|
||||
var resource = applicationContext.getResource(ResourceUtils.CLASSPATH_URL_PREFIX + protocolLocation);
|
||||
try {
|
||||
var xmlProtocols = DomUtils.inputStream2Object(resource.getInputStream(), XmlProtocols.class);
|
||||
@@ -89,7 +86,7 @@ public class PacketService implements IPacketService {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
// 注册协议接收器
|
||||
// Register protocol receivers
|
||||
var componentBeans = applicationContext.getBeansWithAnnotation(Component.class);
|
||||
for (var bean : componentBeans.values()) {
|
||||
NetContext.getRouter().registerPacketReceiverDefinition(bean);
|
||||
@@ -97,7 +94,7 @@ public class PacketService implements IPacketService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取要生成协议列表
|
||||
* Parse and return the set of code languages to generate protocols for.
|
||||
*/
|
||||
private Set<CodeLanguage> getProtocolList(String codeLanguage) {
|
||||
var languageSet = new HashSet<CodeLanguage>();
|
||||
@@ -118,11 +115,11 @@ public class PacketService implements IPacketService {
|
||||
|
||||
@Override
|
||||
public DecodedPacketInfo read(ByteBuf buffer) {
|
||||
// 包的长度在上一层已经解析过
|
||||
// The packet length has already been parsed in the upper layer
|
||||
|
||||
// 解析包体
|
||||
// Parse the packet body
|
||||
var packet = ProtocolManager.read(buffer);
|
||||
// 解析包的附加包
|
||||
// Parse the packet attachment
|
||||
var hasAttachment = ByteBufUtils.tryReadBool(buffer);
|
||||
var attachment = hasAttachment ? (ProtocolManager.read(buffer)) : null;
|
||||
return DecodedPacketInfo.valueOf(packet, attachment);
|
||||
@@ -130,15 +127,15 @@ public class PacketService implements IPacketService {
|
||||
|
||||
@Override
|
||||
public void write(ByteBuf buffer, Object packet, Object attachment) {
|
||||
// 写入包packet
|
||||
// Write the packet
|
||||
ProtocolManager.write(buffer, packet);
|
||||
|
||||
// 写入包的附加包attachment
|
||||
// Write the attachment
|
||||
if (attachment == null) {
|
||||
ByteBufUtils.writeBool(buffer, false);
|
||||
} else {
|
||||
ByteBufUtils.writeBool(buffer, true);
|
||||
// 写入包的附加包attachment
|
||||
// Write the attachment
|
||||
ProtocolManager.write(buffer, attachment);
|
||||
}
|
||||
}
|
||||
@@ -146,7 +143,7 @@ public class PacketService implements IPacketService {
|
||||
@Override
|
||||
public void writeHeaderAndBody(ByteBuf buffer, Object packet, Object attachment) {
|
||||
try {
|
||||
// 预留写入包的长度,一个int字节大小
|
||||
// Reserve space for the packet length header (4 bytes)
|
||||
buffer.ensureWritable(7);
|
||||
buffer.writerIndex(PACKET_HEAD_LENGTH);
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ import com.zfoo.protocol.util.StringUtils;
|
||||
|
||||
/**
|
||||
*
|
||||
* EN: Generic returns can be used both remotely and nested in other protocols
|
||||
* CN: 通用的返回,既可以用在远程调用,又可以嵌套在其它协议里
|
||||
* Generic response that can be used both in remote calls and nested inside other protocol packets.
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
@@ -27,7 +26,7 @@ import com.zfoo.protocol.util.StringUtils;
|
||||
public class Message {
|
||||
|
||||
/**
|
||||
* 1是成功,其它的均视为失败的请求
|
||||
* 1 means success; any other value is treated as a failed request.
|
||||
*/
|
||||
private int code;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import com.zfoo.protocol.anno.Protocol;
|
||||
public class Pong {
|
||||
|
||||
/**
|
||||
* 服务器当前的时间戳
|
||||
* The current server timestamp.
|
||||
*/
|
||||
private long time;
|
||||
|
||||
|
||||
@@ -25,16 +25,14 @@ import org.springframework.lang.Nullable;
|
||||
public interface IRouter {
|
||||
|
||||
/**
|
||||
* EN: send messages entry
|
||||
* CN: 发送消息的入口
|
||||
* Entry point for sending messages.
|
||||
*/
|
||||
void send(Session session, Object packet);
|
||||
|
||||
void send(Session session, Object packet, @Nullable Object attachment);
|
||||
|
||||
/**
|
||||
* EN: receive messages entry
|
||||
* CN: 接收消息的入口
|
||||
* Entry point for receiving messages.
|
||||
*/
|
||||
void receive(Session session, Object packet, @Nullable Object attachment);
|
||||
|
||||
@@ -43,16 +41,19 @@ public interface IRouter {
|
||||
void registerPacketReceiverDefinition(Object bean);
|
||||
|
||||
/**
|
||||
* 服务器对每个syncAsk和asyncAsk请求消息也只能回复一条消息,不能在处理一条不同或者异步消息的时候回复多条消息。
|
||||
* The server can only reply with one message per syncAsk/asyncAsk request.
|
||||
* Multiple replies to a single async message are not allowed.
|
||||
*
|
||||
* @param session 一个网络通信的会话
|
||||
* @param packet 一个网络通信包,需要发送的消息体
|
||||
* @param answerClass 等待返回包的class类。
|
||||
* 如果为null,则不会检查这个class类的协议号是否和返回消息体的协议号相等;
|
||||
* 如果不为null,会检查返回包的协议号。为null的情况主要用在网关。
|
||||
* @param argument 参数,主要用来计算一致性hashId,服务器收到请求过后会使用这个参数来计算再哪个线程执行任务;
|
||||
* @return 服务器返回的消息Response
|
||||
* @throws Exception 如果超时或者其它异常
|
||||
* @param session the network session
|
||||
* @param packet the request packet to send
|
||||
* @param answerClass the expected response class.
|
||||
* If null, the protocol ID of the response is not verified;
|
||||
* if not null, the response protocol ID will be checked.
|
||||
* Null is mainly used in gateway scenarios.
|
||||
* @param argument parameter for computing the consistent-hash ID; the server uses this to determine
|
||||
* which thread the task runs on after receiving the request.
|
||||
* @return the server's response message
|
||||
* @throws Exception if the request times out or another exception occurs
|
||||
*/
|
||||
<T> SyncAnswer<T> syncAsk(Session session, Object packet, @Nullable Class<T> answerClass, @Nullable Object argument) throws Exception;
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import com.zfoo.net.enhance.EnhanceUtils;
|
||||
import com.zfoo.net.enhance.IPacketReceiver;
|
||||
import com.zfoo.net.enhance.PacketReceiverDefinition;
|
||||
import com.zfoo.net.packet.EncodedPacketInfo;
|
||||
import com.zfoo.net.packet.PacketService;
|
||||
import com.zfoo.net.packet.common.Error;
|
||||
import com.zfoo.net.packet.common.Heartbeat;
|
||||
import com.zfoo.net.router.answer.AsyncAnswer;
|
||||
@@ -66,14 +65,14 @@ public class Router implements IRouter {
|
||||
protected final ShortObjectHashMap<IPacketReceiver> receiverMap = new ShortObjectHashMap<>();
|
||||
|
||||
/**
|
||||
* 作为服务器接收方,会把receive收到的attachment存储在这个地方,只针对task线程。
|
||||
* atReceiver会设置attachment,但是在方法调用完成会取消,不需要过多关注。
|
||||
* asyncAsk会再次设置attachment,需要重点关注。
|
||||
* As the server receiver, the attachment received in receive() is stored here, applicable only to task threads.
|
||||
* atReceiver sets the attachment and clears it after the method returns, no special attention needed.
|
||||
* asyncAsk sets the attachment again and requires careful attention.
|
||||
*/
|
||||
protected final FastThreadLocalAdapter<Object> serverReceiverAttachmentThreadLocal = new FastThreadLocalAdapter<>();
|
||||
|
||||
/**
|
||||
* 在服务端收到数据后,会调用这个方法. 这个方法在BaseRouteHandler.java的channelRead中被调用
|
||||
* Called after the server receives data. This method is invoked from channelRead in BaseRouteHandler.java.
|
||||
*/
|
||||
@Override
|
||||
public void receive(Session session, Object packet, @Nullable Object attachment) {
|
||||
@@ -84,18 +83,18 @@ public class Router implements IRouter {
|
||||
|
||||
var task = new PacketReceiverTask(session, packet, attachment);
|
||||
if (attachment == null) {
|
||||
// 正常发送消息的接收,把客户端的业务请求包装下到路由策略指定的线程进行业务处理
|
||||
// 客户端以asyncAsk发送请求会携带attachment,服务器也是进入这个receive方法,不会直接return
|
||||
// Normal message reception: wrap the client's request and dispatch it to the thread specified by the routing strategy
|
||||
// When the client sends a request via asyncAsk, the attachment is non-null and the server also enters this receive method without returning early
|
||||
dispatchBySession(task);
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送者(客户端)同步和异步消息的接收,发送者通过signalId判断重复
|
||||
// Reception of sync/async responses for the sender (client), deduplicated by signalId
|
||||
if (attachment.getClass() == SignalAttachment.class) {
|
||||
var signalAttachment = (SignalAttachment) attachment;
|
||||
|
||||
if (signalAttachment.getClient() == SignalAttachment.SIGNAL_OUTSIDE_CLIENT) {
|
||||
// 服务器收到外部客户端的SIGNAL_OUTSIDE_CLIENT,不做任何处理
|
||||
// Server received SIGNAL_OUTSIDE_CLIENT from an external client, no special handling needed
|
||||
dispatchBySession(task);
|
||||
} else if (signalAttachment.getClient() == SignalAttachment.SIGNAL_NATIVE_ARGUMENT_CLIENT) {
|
||||
signalAttachment.setClient(SignalAttachment.SIGNAL_SERVER);
|
||||
@@ -104,14 +103,14 @@ public class Router implements IRouter {
|
||||
signalAttachment.setClient(SignalAttachment.SIGNAL_SERVER);
|
||||
dispatchBySession(task);
|
||||
} else {
|
||||
// 客户端收到服务器应答,客户端发送的时候client为SIGNAL_NATIVE_CLIENT,服务器收到的时候将其设置为SIGNAL_SERVER
|
||||
// Client received a response from the server; client sets it to SIGNAL_NATIVE_CLIENT when sending, server changes it to SIGNAL_SERVER
|
||||
var removedAttachment = (SignalAttachment) SignalBridge.removeSignalAttachment(signalAttachment);
|
||||
if (removedAttachment == null) {
|
||||
logger.error("client receives packet:[{}] [{}] and attachment:[{}] [{}] from server, but clientAttachmentMap has no attachment, perhaps timeout exception."
|
||||
, packet.getClass().getSimpleName(), JsonUtils.object2String(packet), attachment.getClass(), JsonUtils.object2String(attachment));
|
||||
return;
|
||||
}
|
||||
// 这里会让之前的CompletableFuture得到结果,从而像asyncAsk之类的回调到结果
|
||||
// Completes the CompletableFuture so that asyncAsk callbacks can receive the result
|
||||
removedAttachment.getResponseFuture().complete(packet);
|
||||
}
|
||||
return;
|
||||
@@ -120,13 +119,13 @@ public class Router implements IRouter {
|
||||
if (attachment.getClass() == GatewayAttachment.class) {
|
||||
var gatewayAttachment = (GatewayAttachment) attachment;
|
||||
|
||||
// 如:在网关监听到GatewaySessionInactiveEvent后,这时告诉home时,这个client参数设置的true
|
||||
// 注意:此时并没有return,这样子网关的消息才能发给home,在home进行处理LogoutRequest消息的处理
|
||||
// For example: after the gateway detects GatewaySessionInactiveEvent and notifies the home server,
|
||||
// client is set to true. Note: no return here so the message can be forwarded from gateway to home for LogoutRequest handling.
|
||||
if (gatewayAttachment.isClient()) {
|
||||
gatewayAttachment.setClient(false);
|
||||
dispatchByTaskExecutorHash(gatewayAttachment.taskExecutorHash(), task);
|
||||
} else {
|
||||
// 这里是:别的服务提供者提供授权给网关,比如:在用户或玩家登录后,home服查到了玩家uid,然后发给Gateway服
|
||||
// Another service provider sends authorization to the gateway, e.g., after login the home server looks up the player's uid and sends it to the gateway
|
||||
var gatewaySession = NetContext.getSessionManager().getServerSession(gatewayAttachment.getSid());
|
||||
if (gatewaySession == null) {
|
||||
logger.error("gateway receives packet:[{}] and attachment:[{}] from server" + ", but serverSessionMap has no session[id:{}], perhaps client disconnected from gateway.", JsonUtils.object2String(packet), JsonUtils.object2String(attachment), gatewayAttachment.getSid());
|
||||
@@ -147,24 +146,30 @@ public class Router implements IRouter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Actor模型,最主要的就是线程模型,Actor模型保证了某个Actor所代表的任务永远不会同时在两条线程同时处理任务,这就避免了并发。
|
||||
* 无论是Java,Kotlin,Scala都没有真正的协程,所以最终做到Actor模型的只能是细致的控制线程。
|
||||
* The most important aspect of the Actor model is the threading model. The Actor model guarantees that
|
||||
* tasks represented by an Actor are never processed on two threads simultaneously, thereby avoiding concurrency issues.
|
||||
* Java, Kotlin, and Scala do not have true coroutines, so the Actor model can only be achieved through fine-grained thread control.
|
||||
* <p>
|
||||
* zfoo中通过对线程池的细粒度控制,从而实现了Actor模型。
|
||||
* 为了简单,可以把Actor可以理解为一个用户或者一个玩家。
|
||||
* 因为同一个用户或者玩家的uid是固定的,通过uid去计算一致性hash(taskExecutorHash)永远会得到一致的结果(如果没有uid就用session id),
|
||||
* 从而保证同一个用户或者玩家的请求总能通过taskExecutorHash被路由到同一台服务器的同一个线程去执行,从而避免了并发,实现了无锁化。
|
||||
* zfoo implements the Actor model through fine-grained thread pool control.
|
||||
* An Actor can be understood simply as a user or a player.
|
||||
* Because the uid of the same user/player is fixed, computing the consistent hash (taskExecutorHash) from uid always yields
|
||||
* the same result (sid is used if uid is unavailable), ensuring that all requests from the same user/player are always
|
||||
* routed to the same thread on the same server via taskExecutorHash, thus eliminating concurrency and achieving lock-free design.
|
||||
* <p>
|
||||
* zfoo所代表的Actor模型,是更加精简的Actor模型,让上层调用无感知,在zfoo中可以简单的理解 actor = taskExecutorHash。
|
||||
* The Actor model in zfoo is a streamlined version that is transparent to the caller. In zfoo, actor = taskExecutorHash.
|
||||
* <p>
|
||||
* 在zfoo这套线程模型中,保证了服务器所接收到的Packet(最终被包装成PacketReceiverTask任务),永远只会在同一条线程处理,
|
||||
* TaskBus通过AbstractTaskDispatch去派发PacketReceiverTask任务,具体在哪个线程处理通过IAttachment的taskExecutorHash计算。
|
||||
* In this threading model, packets received by the server (wrapped as PacketReceiverTask) are always processed
|
||||
* on the same thread. TaskBus dispatches PacketReceiverTask via AbstractTaskDispatch, and the target thread is
|
||||
* determined by the taskExecutorHash from IAttachment.
|
||||
* <p>
|
||||
* 这种流水线做法对cpu缓存非常友好,java线程能大部分时间跑在一个cpu核心,而用户逻辑又和线程一一对应,这样就可以最大限度提高cpu缓存命中率。
|
||||
* cpu的cache越大命中率就越高,性能提高就越明显。
|
||||
* This pipeline approach is very CPU-cache-friendly: Java threads mostly run on the same CPU core,
|
||||
* and user logic maps one-to-one to threads, maximizing CPU cache hit rate.
|
||||
* The larger the CPU cache, the higher the hit rate, and the more significant the performance improvement.
|
||||
* <p>
|
||||
* 单线程热点问题,在负载足够大的情况下,比如5000人同时在线的8核服务器,因为样本足够大每个核心分配的人数差距并不会太大。
|
||||
* 概率论告诉我们样本大的话分布是均匀的,小概率事件的单线程热点问题可以忽略,实在不行就加线程。
|
||||
* Regarding single-thread hotspot issues: under sufficient load (e.g., 5000 concurrent users on an 8-core server),
|
||||
* the sample size is large enough that the number of users per core is fairly balanced.
|
||||
* Probability theory tells us that large samples yield uniform distributions, so single-thread hotspot issues
|
||||
* are rare and can be ignored; otherwise just add more threads.
|
||||
*/
|
||||
public void dispatchBySession(PacketReceiverTask task) {
|
||||
var session = task.getSession();
|
||||
@@ -212,7 +217,7 @@ public class Router implements IRouter {
|
||||
|
||||
@Override
|
||||
public void send(Session session, Object packet) {
|
||||
// 服务器异步返回的消息的发送会有signalAttachment,验证返回的消息是否满足
|
||||
// When the server sends an async response, there may be a signalAttachment to verify the response
|
||||
var serverSignalAttachment = serverReceiverAttachmentThreadLocal.get();
|
||||
send(session, packet, serverSignalAttachment);
|
||||
}
|
||||
@@ -236,7 +241,7 @@ public class Router implements IRouter {
|
||||
try {
|
||||
SignalBridge.addSignalAttachment(clientSignalAttachment);
|
||||
|
||||
// 里面调用的依然是:send方法发送消息
|
||||
// Internally still calls the send method to send the message
|
||||
send(session, packet, clientSignalAttachment);
|
||||
|
||||
Object responsePacket = clientSignalAttachment.getResponseFuture().get(timeoutMillis, TimeUnit.MILLISECONDS);
|
||||
@@ -259,9 +264,10 @@ public class Router implements IRouter {
|
||||
}
|
||||
|
||||
/**
|
||||
* 注意:
|
||||
* 1.这个里面其实还是调用send发送的消息
|
||||
* 2.这个argument的参数,只用于provider处哪个线程执行,其实就是hashId,如:工会业务,则传入guildId,回调回来后,一定会在发起者线程。
|
||||
* Note:
|
||||
* 1. Internally this still calls send() to deliver the message.
|
||||
* 2. The argument parameter only determines which thread executes on the provider side (it is essentially a hashId).
|
||||
* For example, pass guildId for guild logic. After the callback, execution is guaranteed to return to the caller's thread.
|
||||
*/
|
||||
@Override
|
||||
public <T> AsyncAnswer<T> asyncAsk(Session session, Object packet, @Nullable Class<T> answerClass, @Nullable Object argument) {
|
||||
@@ -279,7 +285,7 @@ public class Router implements IRouter {
|
||||
clientSignalAttachment.setTaskExecutorHash(TaskBus.calTaskExecutorHash(argument));
|
||||
}
|
||||
|
||||
// 服务器在同步或异步的消息处理中,又调用了同步或异步的方法,这时候threadReceiverAttachment不为空
|
||||
// If the server calls a sync/async method within another sync/async handler, serverSignalAttachment will be non-null
|
||||
var serverSignalAttachment = serverReceiverAttachmentThreadLocal.get();
|
||||
|
||||
try {
|
||||
@@ -287,7 +293,7 @@ public class Router implements IRouter {
|
||||
asyncAnswer.setSignalAttachment(clientSignalAttachment);
|
||||
|
||||
clientSignalAttachment.getResponseFuture()
|
||||
.completeOnTimeout(null, timeoutMillis, TimeUnit.MILLISECONDS) // 因此超时的情况,返回的是null
|
||||
.completeOnTimeout(null, timeoutMillis, TimeUnit.MILLISECONDS) // On timeout, the result is null
|
||||
.thenApply(answer -> {
|
||||
if (answer == null) {
|
||||
throw new NetTimeOutException("async ask [{}] timeout exception", packet.getClass().getSimpleName());
|
||||
@@ -303,16 +309,17 @@ public class Router implements IRouter {
|
||||
return answer;
|
||||
})
|
||||
.whenCompleteAsync((answer, throwable) -> {
|
||||
// 注意:进入这个方法的时机是:在上面的receive方法中,由于是asyncAsk的消息,attachment不为空,会调用CompletableFuture的complete方法
|
||||
// Note: this callback is triggered when receive() completes the CompletableFuture
|
||||
// because the asyncAsk message carries a non-null attachment
|
||||
try {
|
||||
SignalBridge.removeSignalAttachment(clientSignalAttachment);
|
||||
|
||||
// 接收者在同步或异步的消息处理中,又调用了异步的方法,这时候threadServerAttachment不为空
|
||||
// If the receiver calls another async method within a sync/async handler, serverSignalAttachment will be non-null
|
||||
if (serverSignalAttachment != null) {
|
||||
serverReceiverAttachmentThreadLocal.set(serverSignalAttachment);
|
||||
}
|
||||
|
||||
// 如果有异常的话,whenCompleteAsync的下一个thenAccept不会执行
|
||||
// If there is an exception, the subsequent thenAccept will not be executed
|
||||
if (throwable != null) {
|
||||
var notCompleteCallback = asyncAnswer.getNotCompleteCallback();
|
||||
if (notCompleteCallback != null) {
|
||||
@@ -323,13 +330,13 @@ public class Router implements IRouter {
|
||||
return;
|
||||
}
|
||||
|
||||
// 异步返回,回调业务逻辑
|
||||
// Async response received, invoke the business callback
|
||||
@SuppressWarnings("unchecked")
|
||||
var answerPacket = (T) answer;
|
||||
asyncAnswer.setFuturePacket(answerPacket);
|
||||
asyncAnswer.consume();
|
||||
} catch (Throwable throwable1) {
|
||||
logger.error("Asynchronous callback method [ask:{}][answer:{}] error", packet.getClass().getSimpleName(), answer.getClass().getSimpleName(), throwable1);
|
||||
logger.error("Async callback method [ask:{}][answer:{}] error", packet.getClass().getSimpleName(), answer.getClass().getSimpleName(), throwable1);
|
||||
} finally {
|
||||
if (serverSignalAttachment != null) {
|
||||
serverReceiverAttachmentThreadLocal.set(null);
|
||||
@@ -341,7 +348,7 @@ public class Router implements IRouter {
|
||||
|
||||
SignalBridge.addSignalAttachment(clientSignalAttachment);
|
||||
|
||||
// 等到上层调用whenComplete才会发送消息
|
||||
// The message is sent only after the caller invokes whenComplete
|
||||
asyncAnswer.setAskCallback(() -> send(session, packet, clientSignalAttachment));
|
||||
return asyncAnswer;
|
||||
} catch (Exception e) {
|
||||
@@ -352,10 +359,10 @@ public class Router implements IRouter {
|
||||
|
||||
|
||||
/**
|
||||
* 正常消息的接收
|
||||
* Normal message reception.
|
||||
* <p>
|
||||
* 发送者同时能发送多个包
|
||||
* 接收者同时只能处理一个session的一个包,同一个发送者发送过来的包排队处理
|
||||
* A sender can send multiple packets simultaneously.
|
||||
* A receiver processes only one packet from a session at a time; packets from the same sender are queued.
|
||||
*/
|
||||
@Override
|
||||
public void atReceiver(PacketReceiverTask packetReceiverTask) {
|
||||
@@ -366,10 +373,10 @@ public class Router implements IRouter {
|
||||
// The routing of the message
|
||||
var receiver = receiverMap.get(ProtocolManager.protocolId(packet.getClass()));
|
||||
try {
|
||||
// 接收者(服务器)同步和异步消息的接收
|
||||
// The receiver (server) sets up the attachment for sync/async message handling
|
||||
serverReceiverAttachmentThreadLocal.set(attachment);
|
||||
|
||||
// 虚拟线程赋值给ThreadLocal会出错,这里给使用者一个提示
|
||||
// Assigning ThreadLocal inside a virtual thread may cause errors; provide a warning to users
|
||||
if (receiver.task() == Task.VirtualThread && receiver.attachment() == null) {
|
||||
logger.warn("virtual thread task can not set Attachment to ThreadLocal, may cause some sync and async timeout exception, please use attachment in receiver method signature and use attachment in sync and async request");
|
||||
}
|
||||
@@ -380,7 +387,7 @@ public class Router implements IRouter {
|
||||
} catch (Throwable t) {
|
||||
throwableHandler(t, packetReceiverTask);
|
||||
} finally {
|
||||
// 如果有服务器在处理同步或者异步消息的时候由于错误没有返回给客户端消息,则可能会残留serverAttachment,所以先移除
|
||||
// If the server fails to respond during sync/async processing, a stale serverAttachment may remain; clear it here
|
||||
serverReceiverAttachmentThreadLocal.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
|
||||
/**
|
||||
* EN:Synchronous or asynchronous call controller, synchronous and asynchronous call signal communication bridge
|
||||
* CN:同步或异步的调用控制器,同步和异步调用的信号沟通桥梁
|
||||
* Synchronous or asynchronous call controller, acting as a signal communication bridge for sync and async calls
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
@@ -30,12 +29,11 @@ public class SignalBridge {
|
||||
private static final int SIGNAL_MASK = 0B00000000_00000000_01111111_11111111;
|
||||
|
||||
/**
|
||||
* key:signalId
|
||||
* key: signalId
|
||||
*/
|
||||
private static final Map<Integer, SignalAttachment> signalAttachmentMap = new ConcurrentHashMap<>(1000);
|
||||
/**
|
||||
* EN:Ring buffer data structure, which is cache-friendly, can reduce cache contention and improve memory access efficiency
|
||||
* CN:环形缓冲区数据结构,该数据结构具有良好的缓存友好性,可以充分利用 CPU 缓存,减少缓存竞争,提高内存访问效率
|
||||
* Ring buffer data structure, which is cache-friendly, reduces cache contention and improves memory access efficiency
|
||||
*/
|
||||
private static final AtomicReferenceArray<SignalAttachment> signalAttachmentArray = new AtomicReferenceArray<>(SIGNAL_MASK + 1);
|
||||
|
||||
@@ -43,7 +41,7 @@ public class SignalBridge {
|
||||
var signalId = signalAttachment.getSignalId();
|
||||
var hash = signalId & SIGNAL_MASK;
|
||||
|
||||
// Using an Atomic Reference Array is just to improve performance, and only using a ConcurrentHashMap will still work
|
||||
// Using an AtomicReferenceArray is just to improve performance; using only a ConcurrentHashMap would also work correctly
|
||||
if (signalAttachmentArray.compareAndSet(hash, null, signalAttachment)) {
|
||||
return;
|
||||
}
|
||||
@@ -60,7 +58,7 @@ public class SignalBridge {
|
||||
|
||||
var attachment = signalAttachmentArray.get(hash);
|
||||
if (attachment != null && attachment.getSignalId() == signalId) {
|
||||
// 使用性能更高的lazySet(),当我们想对一个原子变量进行修改,而且我们知道这个修改不需要立即对其他线程可见。
|
||||
// Use the higher-performance lazySet() when the modification doesn't need to be immediately visible to other threads
|
||||
signalAttachmentArray.lazySet(hash, null);
|
||||
return attachment;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ public class AsyncAnswer<T> implements IAsyncAnswer<T> {
|
||||
public void whenComplete(Consumer<T> consumer) {
|
||||
thenAccept(consumer);
|
||||
|
||||
// 这里其实会触发发送消息
|
||||
// This actually triggers sending the message
|
||||
askCallback.run();
|
||||
}
|
||||
|
||||
|
||||
@@ -24,14 +24,12 @@ public interface IAsyncAnswer<T> {
|
||||
IAsyncAnswer<T> thenAccept(Consumer<T> consumer);
|
||||
|
||||
/**
|
||||
* EN:To receive the message returned asynchronously and process the message, the asynchronous request must call this method
|
||||
* CN:接收到异步返回的消息,并处理这个消息,异步请求必须要调用这个方法
|
||||
* Receives the async response message and processes it; must be called for every async request.
|
||||
*/
|
||||
void whenComplete(Consumer<T> consumer);
|
||||
|
||||
/**
|
||||
* EN:If the asynchronous request does not return successfully, the method is called
|
||||
* CN:如果异步请求没有成功返回,那么就会回调该方法
|
||||
* Called if the asynchronous request does not return successfully.
|
||||
*/
|
||||
IAsyncAnswer<T> notComplete(Runnable notCompleteCallback);
|
||||
|
||||
|
||||
@@ -27,16 +27,13 @@ public class GatewayAttachment {
|
||||
private long sid;
|
||||
|
||||
/**
|
||||
* EN:User ID, the message forwarded from the gateway to the back must be accompanied by the user's ID information,
|
||||
* otherwise it is impossible to know which user sent it, 0 means no user ID
|
||||
* <p>
|
||||
* CN:用戶Id,从网关转发到后面的消息必须要附带用户的Id信息,要不然无法知道是哪个用户发过来的,0代表没有用户id
|
||||
* User ID. Messages forwarded from the gateway must carry the user's ID so that downstream services
|
||||
* know which user sent the message. 0 means no user ID.
|
||||
*/
|
||||
private long uid;
|
||||
|
||||
/**
|
||||
* EN:Used to determine which thread the message is processed on
|
||||
* CN:用来确定这条消息在哪一个线程处理
|
||||
* Determines which thread this message is processed on.
|
||||
*/
|
||||
private int taskExecutorHash;
|
||||
|
||||
@@ -47,8 +44,8 @@ public class GatewayAttachment {
|
||||
|
||||
|
||||
/**
|
||||
* EN:The client may send an packet with synchronous or asynchronous to the gateway, and the gateway needs to bring this attachment when forwarding
|
||||
* CN:客户端发到网关的可能是一个带有同步或者异步的附加包,网关转发的时候需要把这个附加包给带上
|
||||
* A client packet sent to the gateway may carry a synchronous or asynchronous attachment.
|
||||
* The gateway must forward this attachment along with the packet.
|
||||
*/
|
||||
private SignalAttachment signalAttachment;
|
||||
|
||||
@@ -69,8 +66,8 @@ public class GatewayAttachment {
|
||||
|
||||
|
||||
/**
|
||||
* EN:Used to determine which thread the message is processed on
|
||||
* CN:用来确定这条消息在哪一个线程处理
|
||||
* Determines which thread this message is processed on.
|
||||
* Falls back to uid when taskExecutorHash is 0.
|
||||
*/
|
||||
public int taskExecutorHash() {
|
||||
return taskExecutorHash == 0 ? (int) uid : taskExecutorHash;
|
||||
|
||||
@@ -39,8 +39,7 @@ public class HttpAttachment {
|
||||
|
||||
|
||||
/**
|
||||
* EN:Used to determine which thread the message is processed on
|
||||
* CN:用来确定这条消息在哪一个线程处理
|
||||
* Determines which thread will process this message.
|
||||
*/
|
||||
public int taskExecutorHash() {
|
||||
return taskExecutorHash == 0 ? (int) uid : taskExecutorHash;
|
||||
|
||||
@@ -29,8 +29,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
public class SignalAttachment {
|
||||
|
||||
/**
|
||||
* EN:Negative signalId are allowed
|
||||
* CN:允许负数的signalId
|
||||
* Negative signalId values are allowed.
|
||||
*/
|
||||
public static final AtomicInteger ATOMIC_ID = new AtomicInteger(0);
|
||||
/**
|
||||
@@ -42,14 +41,13 @@ public class SignalAttachment {
|
||||
public static final byte SIGNAL_OUTSIDE_CLIENT = 12;
|
||||
|
||||
/**
|
||||
* EN:Unique identification of a packet, unique representation of an attachment, hashcode() and equals() equals signalId value
|
||||
* CN:唯一标识一个packet, 唯一表示一个Attachment,hashcode() and equals() 等于signalId
|
||||
* Uniquely identifies a packet and its attachment.
|
||||
* hashCode() and equals() are based on signalId.
|
||||
*/
|
||||
private int signalId = ATOMIC_ID.incrementAndGet();
|
||||
|
||||
/**
|
||||
* EN:The parameter used to calculate the hash in TaskBus to determine which thread the task is executed on
|
||||
* CN:用来在TaskBus中计算hash的参数,用来决定任务在哪一条线程执行
|
||||
* The hash parameter used by TaskBus to determine which thread the task runs on.
|
||||
*/
|
||||
private int taskExecutorHash = -1;
|
||||
|
||||
@@ -62,8 +60,7 @@ public class SignalAttachment {
|
||||
private long timestamp = TimeUtils.now();
|
||||
|
||||
/**
|
||||
* EN:The method of callback when the client receives a reply from the server
|
||||
* CN:客户端收到服务器回复的时候回调的方法
|
||||
* The callback invoked when the client receives a reply from the server.
|
||||
*/
|
||||
@JsonIgnore
|
||||
private transient CompletableFuture<Object> responseFuture = new CompletableFuture<>();
|
||||
|
||||
@@ -42,39 +42,39 @@ public class NetDefinitionParser implements BeanDefinitionParser {
|
||||
String name;
|
||||
BeanDefinitionBuilder builder;
|
||||
|
||||
// 解析NetConfig的配置
|
||||
// Parse the NetConfig configuration
|
||||
parseNetConfig(element, parserContext);
|
||||
|
||||
// 注册NetContext
|
||||
// Register NetContext
|
||||
clazz = NetContext.class;
|
||||
name = StringUtils.uncapitalize(clazz.getName());
|
||||
builder = BeanDefinitionBuilder.rootBeanDefinition(clazz);
|
||||
parserContext.getRegistry().registerBeanDefinition(name, builder.getBeanDefinition());
|
||||
|
||||
// 注册ConfigManager
|
||||
// Register ConfigManager and wire in the NetConfig bean as its localConfig property
|
||||
clazz = ConfigManager.class;
|
||||
builder = BeanDefinitionBuilder.rootBeanDefinition(clazz);
|
||||
// 把Spring容器中的NetConfig引用赋值给ConfigManager中的localConfig属性
|
||||
builder.addPropertyReference("localConfig", NetConfig.class.getCanonicalName());
|
||||
parserContext.getRegistry().registerBeanDefinition(clazz.getCanonicalName(), builder.getBeanDefinition());
|
||||
|
||||
// 注册PacketService
|
||||
// Register PacketService
|
||||
clazz = PacketService.class;
|
||||
builder = BeanDefinitionBuilder.rootBeanDefinition(clazz);
|
||||
parserContext.getRegistry().registerBeanDefinition(clazz.getCanonicalName(), builder.getBeanDefinition());
|
||||
|
||||
// 注册Router
|
||||
// Register Router
|
||||
clazz = Router.class;
|
||||
builder = BeanDefinitionBuilder.rootBeanDefinition(clazz);
|
||||
parserContext.getRegistry().registerBeanDefinition(clazz.getCanonicalName(), builder.getBeanDefinition());
|
||||
|
||||
// 注册Consumer
|
||||
// Register Consumer
|
||||
clazz = Consumer.class;
|
||||
builder = BeanDefinitionBuilder.rootBeanDefinition(clazz);
|
||||
parserContext.getRegistry().registerBeanDefinition(clazz.getCanonicalName(), builder.getBeanDefinition());
|
||||
|
||||
// 注册SessionManager
|
||||
// 里面的serverSessionMap 和 clientSessionMap 是根据自己是客户端还是服务器保存连接自己 和 自己连接 的网络节点信息
|
||||
// Register SessionManager
|
||||
// serverSessionMap: sessions of clients connected to this node (server role)
|
||||
// clientSessionMap: sessions this node established to remote providers (client role)
|
||||
clazz = SessionManager.class;
|
||||
builder = BeanDefinitionBuilder.rootBeanDefinition(clazz);
|
||||
parserContext.getRegistry().registerBeanDefinition(clazz.getCanonicalName(), builder.getBeanDefinition());
|
||||
@@ -84,63 +84,63 @@ public class NetDefinitionParser implements BeanDefinitionParser {
|
||||
|
||||
|
||||
/**
|
||||
* 解析config标签,并且注册NetConfig类到Spring容器
|
||||
* Parse the <config> element and register NetConfig as a Spring bean.
|
||||
*
|
||||
* @param element
|
||||
* @param parserContext
|
||||
* @param element the XML element to parse
|
||||
* @param parserContext the Spring parser context
|
||||
*/
|
||||
private void parseNetConfig(Element element, ParserContext parserContext) {
|
||||
// 要注册到Spring的类,下面都是进行解析自定义标签,把值赋值给NetConfig的属性
|
||||
// The class to register; parse each custom XML attribute and map it to a NetConfig field
|
||||
var clazz = NetConfig.class;
|
||||
var builder = BeanDefinitionBuilder.rootBeanDefinition(clazz);
|
||||
|
||||
// -----config属性解析-----
|
||||
// 解析id
|
||||
// ----- config attribute parsing -----
|
||||
// Parse the node/application id
|
||||
resolvePlaceholder("id", "id", builder, element, parserContext);
|
||||
|
||||
// 协议protocol.xml文件的位置。 注意:直接写protocol.xml 则是从resources目录下读
|
||||
// Location of protocol.xml; writing just "protocol.xml" reads from the resources directory
|
||||
resolvePlaceholder("protocol-location", "protocolLocation", builder, element, parserContext);
|
||||
|
||||
// 协议文件是否生成在同一个协议文件中
|
||||
// Whether all protocols are merged into a single file
|
||||
resolvePlaceholder("merge-protocol", "mergeProtocol", builder, element, parserContext);
|
||||
// 文件是否折叠
|
||||
// Whether the protocol file is folded/collapsed
|
||||
resolvePlaceholder("fold-protocol", "foldProtocol", builder, element, parserContext);
|
||||
// 生成各种语言的协议列表
|
||||
// List of languages for which protocol code should be generated
|
||||
resolvePlaceholder("code-languages", "codeLanguages", builder, element, parserContext);
|
||||
|
||||
resolvePlaceholder("protocol-path", "protocolPath", builder, element, parserContext);
|
||||
|
||||
resolvePlaceholder("protocol-param", "protocolParam", builder, element, parserContext);
|
||||
|
||||
// -----注册中心解析-----
|
||||
// 上面解析的都是config标签的属性,这里开始解析registry元素
|
||||
// ----- registry element parsing (all above were <config> attributes) -----
|
||||
var registryElement = DomUtils.getFirstChildElementByTagName(element, "registry");
|
||||
if (registryElement != null) {
|
||||
parseRegistryConfig(registryElement, parserContext);
|
||||
builder.addPropertyReference("registry", RegistryConfig.class.getCanonicalName());
|
||||
}
|
||||
|
||||
// ----- monitor解析-----
|
||||
// ----- monitor element parsing -----
|
||||
var monitorElement = DomUtils.getFirstChildElementByTagName(element, "monitor");
|
||||
if (monitorElement != null) {
|
||||
parseMonitorConfig(monitorElement, parserContext);
|
||||
builder.addPropertyReference("monitor", MonitorConfig.class.getCanonicalName());
|
||||
}
|
||||
|
||||
// -----服务器生产者解析-----
|
||||
// ----- provider element parsing -----
|
||||
var providerElement = DomUtils.getFirstChildElementByTagName(element, "providers");
|
||||
if (providerElement != null) {
|
||||
parseProviderConfig(providerElement, parserContext);
|
||||
builder.addPropertyReference("provider", ProviderConfig.class.getCanonicalName());
|
||||
}
|
||||
|
||||
// ----- consumer element parsing -----
|
||||
var consumerElement = DomUtils.getFirstChildElementByTagName(element, "consumers");
|
||||
if (consumerElement != null) {
|
||||
parseConsumerConfig(consumerElement, parserContext);
|
||||
builder.addPropertyReference("consumer", ConsumerConfig.class.getCanonicalName());
|
||||
}
|
||||
|
||||
// 注册NetConfig到Spring容器中
|
||||
// Register NetConfig as a Spring bean
|
||||
parserContext.getRegistry().registerBeanDefinition(clazz.getCanonicalName(), builder.getBeanDefinition());
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ public class NetDefinitionParser implements BeanDefinitionParser {
|
||||
var providerModules = parseProviderModules("providers", element, parserContext);
|
||||
builder.addPropertyValue("providers", providerModules);
|
||||
|
||||
// 注册Consumer到Spring容器中
|
||||
// Register ProviderConfig as a Spring bean
|
||||
parserContext.getRegistry().registerBeanDefinition(clazz.getCanonicalName(), builder.getBeanDefinition());
|
||||
}
|
||||
|
||||
|
||||
@@ -34,16 +34,15 @@ public class Session implements Closeable {
|
||||
private Channel channel;
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------
|
||||
// The following are extra parameters, add them yourself if necessary(下面都是额外参数,有需要的自己添加)
|
||||
// The following are extra parameters; add your own fields here if necessary.
|
||||
/**
|
||||
* EN:The default user ID is an ID greater than 0, or equal 0 if there is no login, user extra parameters
|
||||
* CN:默认用户的id都是大于0的id,如果没有登录则等于0,用户额外参数
|
||||
* The default user ID is greater than 0; it equals 0 when the user is not logged in.
|
||||
* This is a user-level extra parameter.
|
||||
*/
|
||||
private long uid = 0;
|
||||
|
||||
/**
|
||||
* EN:Session extra parameters
|
||||
* CN:Session附带的属性参数,消费者的属性
|
||||
* Extra attributes attached to the session, representing consumer-side properties.
|
||||
*/
|
||||
private Register consumerRegister = null;
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import com.zfoo.protocol.collection.concurrent.ConcurrentHashMapLongObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
@@ -28,18 +27,17 @@ public class SessionManager implements ISessionManager {
|
||||
private static final Logger logger = LoggerFactory.getLogger(SessionManager.class);
|
||||
|
||||
/**
|
||||
* EN: As a server, the Session is connected by other clients
|
||||
* CN: 作为服务器,被别的客户端连接的Session
|
||||
* Sessions established when this node acts as a server (connected by other clients).
|
||||
* <p>
|
||||
* 如:自己作为网关,那肯定有一大堆客户端连接,他们连接上来后,就会保存下来这些信息。
|
||||
* 因此:要全局消息广播,其实要用这个Map
|
||||
* For example, when acting as a gateway, a large number of client connections will be stored here.
|
||||
* This map should be used for global message broadcasting.
|
||||
*/
|
||||
private final ConcurrentHashMapLongObject<Session> serverSessionMap = new ConcurrentHashMapLongObject<>(128);
|
||||
|
||||
/**
|
||||
* EN: As a client, connect to another server and save Sessions
|
||||
* CN: 作为客户端,连接别的服务器上后,保存下来的Session
|
||||
* 如:自己配置了Consumer,说明自己作为消费者将要消费远程接口,就会创建一个TcpClient去连接Provider,那么连接上后,就会保存下来到这个Map中
|
||||
* Sessions established when this node acts as a client (connected to other servers).
|
||||
* For example, when configured as a Consumer, a TcpClient will be created to connect to a Provider,
|
||||
* and the resulting session will be stored here.
|
||||
*/
|
||||
private final ConcurrentHashMapLongObject<Session> clientSessionMap = new ConcurrentHashMapLongObject<>(8);
|
||||
|
||||
|
||||
@@ -30,9 +30,8 @@ import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* EN: The Task thread pool is generally used to process customer requests, do some CPU-intensive tasks, and try to avoid some blocking operations;
|
||||
* IO-intensive tasks can be executed in the Event thread pool
|
||||
* CN: Task线程池一般是用来处理客户的请求,做一些cpu密集型任务,尽量避免做一些阻塞操作;IO密集型任务可以放在Event线程池去做
|
||||
* The Task thread pool is used to handle client requests and CPU-intensive tasks.
|
||||
* Avoid performing blocking operations here; IO-intensive tasks should be delegated to the Event thread pool.
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
@@ -40,14 +39,13 @@ public final class TaskBus {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TaskBus.class);
|
||||
|
||||
// EN: The size of the thread pool can also be specified through the provider thread configuration
|
||||
// CN: 线程池的大小,也可以通过provider thread配置指定,会通过EXECUTOR_MASK的与操作提高定位到Executor的性能
|
||||
// The thread pool size can also be configured via the provider's thread setting.
|
||||
// EXECUTOR_MASK is used with a bitwise AND to efficiently locate the target Executor.
|
||||
public static final int EXECUTOR_SIZE;
|
||||
private static final int EXECUTOR_MASK;
|
||||
|
||||
/**
|
||||
* EN: Use different thread pools to achieve isolation between thread pools without affecting each other
|
||||
* CN: 使用不同的线程池,让线程池之间实现隔离,互不影响
|
||||
* Multiple independent thread pools to ensure isolation; failures in one pool do not affect others.
|
||||
*/
|
||||
private static final ExecutorService[] executors;
|
||||
|
||||
@@ -93,9 +91,8 @@ public final class TaskBus {
|
||||
|
||||
|
||||
/**
|
||||
* EN: Use uid, sid, or hashcode to bind tasks to the same thread to improve the cache hit ratio of the CPU
|
||||
* CN: 通过uid,sid,或者hashcode来把任务绑定到相同的线程中来提高cpu的缓存命中率
|
||||
* <a href="https://zhuanlan.zhihu.com/p/439381000">系统性能调优之绑定cpu</a>
|
||||
* Bind tasks to the same thread using uid, sid, or hashCode to improve CPU cache hit ratio.
|
||||
* <a href="https://zhuanlan.zhihu.com/p/439381000">System performance tuning - binding CPU</a>
|
||||
*/
|
||||
private static int calTaskExecutorIndex(int taskExecutorHash) {
|
||||
return taskExecutorHash & EXECUTOR_MASK;
|
||||
@@ -129,7 +126,7 @@ public final class TaskBus {
|
||||
return executors[calTaskExecutorIndex(hash)];
|
||||
}
|
||||
|
||||
// 在task,event,scheduler线程执行的异步请求,请求成功过后依然在相同的线程执行回调任务
|
||||
// Async requests executed in task/event/scheduler threads will continue to run their callbacks on the same thread
|
||||
public static Executor currentThreadExecutor() {
|
||||
var currentThreadId = Thread.currentThread().getId();
|
||||
var executor = ThreadUtils.executorByThreadId(currentThreadId);
|
||||
|
||||
@@ -20,28 +20,28 @@ import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* 带虚拟节点的一致性Hash算法,参考:http://www.zsythink.net/archives/1182
|
||||
* Consistent Hash algorithm with virtual nodes, reference: http://www.zsythink.net/archives/1182
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
|
||||
public class ConsistentHash<K, V> {
|
||||
|
||||
// 真实结点列表,考虑到服务器上线、下线的场景,即添加、删除的场景会比较频繁,这里使用LinkedList会更好
|
||||
// List of real nodes. Since nodes may be added or removed frequently (server up/down), LinkedList would be more efficient here.
|
||||
private List<Pair<K, V>> realNodes = new ArrayList<>();
|
||||
|
||||
// 虚拟节点,key表示虚拟节点的hash值,value表示虚拟节点的名称
|
||||
// Virtual nodes: key is the hash of the virtual node, value is the corresponding real node
|
||||
private TreeMap<Integer, Pair<K, V>> virtualNodeTreeMap = new TreeMap<>();
|
||||
|
||||
// 虚拟节点的数目,数量越大约均匀
|
||||
// Number of virtual nodes per real node; the more virtual nodes, the more uniform the distribution
|
||||
private int virtualNodes = 0;
|
||||
|
||||
public ConsistentHash(List<Pair<K, V>> realNodes, int virtualNodes) {
|
||||
// 先把原始的服务器添加到真实结点列表中
|
||||
// First add all real nodes to the real node list
|
||||
this.realNodes.addAll(realNodes);
|
||||
this.virtualNodes = virtualNodes;
|
||||
|
||||
// 再添加虚拟节点,遍历LinkedList使用foreach循环效率会比较高
|
||||
// Then add virtual nodes; using foreach loop for traversal is more efficient with LinkedList
|
||||
for (var realNode : realNodes) {
|
||||
for (var i = 0; i < this.virtualNodes; i++) {
|
||||
var virtualNode = realNode.getKey().toString() + "&&VN" + i;
|
||||
@@ -52,14 +52,14 @@ public class ConsistentHash<K, V> {
|
||||
}
|
||||
|
||||
|
||||
// 得到应当路由到的结点
|
||||
// Get the real node that the given key should be routed to
|
||||
public Pair<K, V> getRealNode(Object key) {
|
||||
// 得到该key的hash值
|
||||
// Compute the hash value of the key
|
||||
var hash = HashUtils.fnvHash(key);
|
||||
// 第一个Key就是顺时针过去离node最近的那个结点
|
||||
// The first key in the ceiling entry is the closest node clockwise
|
||||
var entry = virtualNodeTreeMap.ceilingEntry(hash);
|
||||
if (Objects.isNull(entry)) {
|
||||
// 如果没有比该key的hash值大的,则从第一个node开始
|
||||
// If no hash value is larger than the key's hash, wrap around to the first node
|
||||
return virtualNodeTreeMap.firstEntry().getValue();
|
||||
}
|
||||
return entry.getValue();
|
||||
|
||||
@@ -31,7 +31,7 @@ public class FastTreeMapIntLong {
|
||||
keys = new int[size];
|
||||
values = new long[size];
|
||||
|
||||
// TreeMap遍历是有序的,直接放入数组中就能直接得到从小到大的有序数组
|
||||
// TreeMap iteration is ordered, placing entries directly into an array yields a sorted ascending array
|
||||
var index = 0;
|
||||
for (var entry : treeMap.entrySet()) {
|
||||
var key = entry.getKey();
|
||||
@@ -60,10 +60,10 @@ public class FastTreeMapIntLong {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在一个从小到大的升序数组中,使用二分查找算法搜索给定的key
|
||||
* Binary search for the given key in an ascending sorted array
|
||||
*
|
||||
* @param key 需要搜索的值
|
||||
* @return 返回搜索到值的数组下标索引,如果没有查找到返回-1
|
||||
* @param key the value to search for
|
||||
* @return the array index of the found value, or -1 if not found
|
||||
*/
|
||||
public int indexOf(int key) {
|
||||
var low = 0;
|
||||
@@ -93,16 +93,16 @@ public class FastTreeMapIntLong {
|
||||
}
|
||||
|
||||
var size = keys.length;
|
||||
// 目标数小于或等于数组的第一个元素
|
||||
// Target is less than or equal to the first element
|
||||
if (key <= keys[0]) {
|
||||
return 0;
|
||||
}
|
||||
// 目标数大于数组的最后元素
|
||||
// Target is greater than the last element
|
||||
if (key > keys[size - 1]) {
|
||||
return size - 1;
|
||||
}
|
||||
|
||||
// 二分查找目标数
|
||||
// Binary search for the target
|
||||
var low = 0;
|
||||
var high = size - 1;
|
||||
var nearestIndex = -1;
|
||||
@@ -120,7 +120,7 @@ public class FastTreeMapIntLong {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到了目标数,那么它的邻近最大值就是目标数本身
|
||||
// If the exact key is found, it is its own nearest ceiling key
|
||||
if (nearestIndex == -1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
package com.zfoo.net.util;
|
||||
|
||||
/**
|
||||
* 改进的hash算法虽然发布比较均匀,但是没有Java自带的hash算法速度快,所以需要知道自己需要什么
|
||||
* The improved hash algorithm distributes values more uniformly but is slower than Java's built-in hash.
|
||||
* Choose based on your requirements.
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
@@ -24,10 +25,10 @@ public abstract class HashUtils {
|
||||
private static final int INIT_HASH = (int) 2166136261L;
|
||||
|
||||
/**
|
||||
* 改进的32位FNV算法1
|
||||
* Improved 32-bit FNV-1 hash algorithm.
|
||||
*
|
||||
* @param data 数组
|
||||
* @return hash结果
|
||||
* @param data byte array to hash
|
||||
* @return hash result
|
||||
*/
|
||||
public static int fnvHash(byte[] data) {
|
||||
var hash = INIT_HASH;
|
||||
@@ -43,10 +44,10 @@ public abstract class HashUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 改进的32位FNV算法1
|
||||
* Improved 32-bit FNV-1 hash algorithm.
|
||||
*
|
||||
* @param object 计算hash的对象,会调用toString方法
|
||||
* @return hash结果
|
||||
* @param object the object to hash; its toString() method will be called
|
||||
* @return hash result
|
||||
*/
|
||||
public static int fnvHash(Object object) {
|
||||
var hash = object.toString().chars().reduce(INIT_HASH, (left, right) -> (left ^ right) * P);
|
||||
|
||||
@@ -26,7 +26,7 @@ import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 网络相关工具
|
||||
* Network-related utility methods.
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
@@ -36,11 +36,11 @@ public class NetUtils {
|
||||
public final static String LOCAL_LOOPBACK_IP = "127.0.0.1";
|
||||
|
||||
/**
|
||||
* 默认最小端口,1024
|
||||
* Default minimum port: 1024
|
||||
*/
|
||||
public static final int PORT_RANGE_MIN = 1024;
|
||||
/**
|
||||
* 默认最大端口,65535
|
||||
* Default maximum port: 65535
|
||||
*/
|
||||
public static final int PORT_RANGE_MAX = 0xFFFF;
|
||||
|
||||
@@ -54,17 +54,17 @@ public class NetUtils {
|
||||
public final static Pattern IPV6 = Pattern.compile("(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))");
|
||||
|
||||
/**
|
||||
* 根据long值获取ip v4地址
|
||||
* Convert a long value to an IPv4 address string.
|
||||
*
|
||||
* @param longIP IP的long表示形式
|
||||
* @return IP V4 地址
|
||||
* @param longIP the long representation of an IP address
|
||||
* @return the IPv4 address string
|
||||
*/
|
||||
public static String longToIpv4(long longIP) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
// 直接右移24位
|
||||
// Shift right 24 bits to get the first octet
|
||||
sb.append((longIP / 1_000_000_000));
|
||||
sb.append(".");
|
||||
// 将高8位置0,然后右移16位
|
||||
// Clear the top 8 bits and shift right 16 bits for the second octet
|
||||
sb.append(longIP / 1_000_000 % 1_000);
|
||||
sb.append(".");
|
||||
sb.append(longIP / 1_000 % 1_000);
|
||||
@@ -74,19 +74,19 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ip地址计算出long型的数据
|
||||
* Convert an IPv4 address string to a long value.
|
||||
*
|
||||
* @param strIP IP V4 地址
|
||||
* @return long值
|
||||
* @param strIP the IPv4 address string
|
||||
* @return the long representation
|
||||
*/
|
||||
public static long ipv4ToLong(String strIP) {
|
||||
if (isValidAddress(strIP)) {
|
||||
long[] ip = new long[4];
|
||||
// 先找到IP地址字符串中.的位置
|
||||
// Find the positions of '.' in the IP address string
|
||||
int position1 = strIP.indexOf(".");
|
||||
int position2 = strIP.indexOf(".", position1 + 1);
|
||||
int position3 = strIP.indexOf(".", position2 + 1);
|
||||
// 将每个.之间的字符串转换成整型
|
||||
// Parse each octet between dots as a long
|
||||
ip[0] = Long.parseLong(strIP.substring(0, position1));
|
||||
ip[1] = Long.parseLong(strIP.substring(position1 + 1, position2));
|
||||
ip[2] = Long.parseLong(strIP.substring(position2 + 1, position3));
|
||||
@@ -100,7 +100,7 @@ public class NetUtils {
|
||||
return IPV4.matcher(address).matches() || IPV6.matcher(address).matches();
|
||||
}
|
||||
|
||||
// 同时支持ipv4和ipv6
|
||||
// Supports both IPv4 and IPv6
|
||||
public static long ipToLong(String ip) {
|
||||
var splits = ip.split(StringUtils.PERIOD_REGEX);
|
||||
var longs = new long[splits.length];
|
||||
@@ -138,19 +138,19 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测本地端口可用性<br>
|
||||
* 来自org.springframework.util.SocketUtils
|
||||
* Checks whether a local port is available.<br>
|
||||
* Inspired by org.springframework.util.SocketUtils.
|
||||
*
|
||||
* @param port 被检测的端口
|
||||
* @return 是否可用
|
||||
* @param port the port to check
|
||||
* @return {@code true} if the port is available
|
||||
*/
|
||||
public static boolean isAvailablePort(int port) {
|
||||
if (!isValidPort(port)) {
|
||||
// 给定的IP未在指定端口范围中
|
||||
// Port number is outside the valid range
|
||||
return false;
|
||||
}
|
||||
|
||||
// try-with-resources会自动调用close方法
|
||||
// try-with-resources automatically calls close()
|
||||
try (ServerSocket ss = new ServerSocket(port, 0, getLocalhost())) {
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
@@ -159,45 +159,45 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为有效的端口<br>
|
||||
* 此方法并不检查端口是否被占用
|
||||
* Checks whether a port number is valid.<br>
|
||||
* This method does NOT check whether the port is already in use.
|
||||
*
|
||||
* @param port 端口号
|
||||
* @return 是否有效
|
||||
* @param port the port number
|
||||
* @return {@code true} if the port is in the range [0, 65535]
|
||||
*/
|
||||
public static boolean isValidPort(int port) {
|
||||
// 有效端口是0~65535
|
||||
// Valid port range: 0 ~ 65535
|
||||
return port >= 0 && port <= PORT_RANGE_MAX;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找1024~65535范围内的可用端口<br>
|
||||
* 此方法只检测给定范围内的随机一个端口,检测65535-1024次<br>
|
||||
* Finds an available port in the range [1024, 65535].<br>
|
||||
* Iterates sequentially up to (65535 - 1024) times.<br>
|
||||
*
|
||||
* @return 可用的端口
|
||||
* @return an available port number
|
||||
*/
|
||||
public static int getAvailablePort() {
|
||||
return getAvailablePort(PORT_RANGE_MIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找指定范围内的可用端口,最大值为65535<br>
|
||||
* 此方法只检测给定范围内的随机一个端口,检测65535-minPort次<br>
|
||||
* Finds an available port in the range [{@code minPort}, 65535].<br>
|
||||
* Iterates sequentially up to (65535 - minPort) times.<br>
|
||||
*
|
||||
* @param minPort 端口最小值(包含)
|
||||
* @return 可用的端口
|
||||
* @param minPort the minimum port number (inclusive)
|
||||
* @return an available port number
|
||||
*/
|
||||
public static int getAvailablePort(int minPort) {
|
||||
return getAvailablePort(minPort, PORT_RANGE_MAX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找指定范围内的可用端口<br>
|
||||
* 此方法只检测给定范围内的随机一个端口,检测maxPort-minPort次<br>
|
||||
* Finds an available port in the range [{@code minPort}, {@code maxPort}].<br>
|
||||
* Iterates sequentially up to (maxPort - minPort) times.<br>
|
||||
*
|
||||
* @param minPort 端口最小值(包含)
|
||||
* @param maxPort 端口最大值(包含)
|
||||
* @return 可用的端口
|
||||
* @param minPort the minimum port number (inclusive)
|
||||
* @param maxPort the maximum port number (inclusive)
|
||||
* @return an available port number
|
||||
*/
|
||||
public static int getAvailablePort(int minPort, int maxPort) {
|
||||
for (int i = minPort; i <= maxPort; i++) {
|
||||
@@ -211,11 +211,12 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多个本地可用端口<br>
|
||||
* Retrieves multiple available local ports.<br>
|
||||
*
|
||||
* @param minPort 端口最小值(包含)
|
||||
* @param maxPort 端口最大值(包含)
|
||||
* @return 可用的端口
|
||||
* @param numRequested the number of ports needed
|
||||
* @param minPort the minimum port number (inclusive)
|
||||
* @param maxPort the maximum port number (inclusive)
|
||||
* @return a sorted set of available port numbers
|
||||
*/
|
||||
public static TreeSet<Integer> getAvailablePorts(int numRequested, int minPort, int maxPort) {
|
||||
final TreeSet<Integer> availablePorts = new TreeSet<>();
|
||||
@@ -233,11 +234,13 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 判定是否为内网IP<br>
|
||||
* 私有IP:A类 10.0.0.0-10.255.255.255 B类 172.16.0.0-172.31.255.255 C类 192.168.0.0-192.168.255.255 当然,还有127这个网段是环回地址
|
||||
* Determines whether an IP address belongs to a private (intranet) range.<br>
|
||||
* Private ranges: Class A 10.0.0.0–10.255.255.255,
|
||||
* Class B 172.16.0.0–172.31.255.255, Class C 192.168.0.0–192.168.255.255,
|
||||
* and the loopback range 127.x.x.x.
|
||||
*
|
||||
* @param ipAddress IP地址
|
||||
* @return 是否为内网IP
|
||||
* @param ipAddress the IP address string
|
||||
* @return {@code true} if the address is a private/loopback address
|
||||
*/
|
||||
public static boolean isInnerIP(String ipAddress) {
|
||||
long ipNum = NetUtils.ipv4ToLong(ipAddress);
|
||||
@@ -256,12 +259,12 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定IP的long是否在指定范围内
|
||||
* Checks whether the numeric representation of an IP address falls within [begin, end].
|
||||
*
|
||||
* @param userIp 用户IP
|
||||
* @param begin 开始IP
|
||||
* @param end 结束IP
|
||||
* @return 是否在范围内
|
||||
* @param userIp the numeric IP to test
|
||||
* @param begin the lower bound (inclusive)
|
||||
* @param end the upper bound (inclusive)
|
||||
* @return {@code true} if {@code userIp} is within the range
|
||||
*/
|
||||
private static boolean isInner(long userIp, long begin, long end) {
|
||||
return (userIp >= begin) && (userIp <= end);
|
||||
@@ -269,10 +272,10 @@ public class NetUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 通过域名得到IP
|
||||
* Resolves a hostname to its IP address string.
|
||||
*
|
||||
* @param hostName HOST
|
||||
* @return ip address or hostName if UnknownHostException
|
||||
* @param hostName the hostname or domain name
|
||||
* @return the resolved IP address, or the original hostName if resolution fails
|
||||
*/
|
||||
public static String getIpByHost(String hostName) {
|
||||
try {
|
||||
@@ -283,7 +286,7 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本机所有网卡
|
||||
* Returns all network interfaces on the local machine.
|
||||
*/
|
||||
public static Set<NetworkInterface> getAllNetworkInterface() {
|
||||
try {
|
||||
@@ -299,9 +302,9 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有满足过滤条件的本地IP地址对象
|
||||
* Returns all local {@link InetAddress} objects across all network interfaces.
|
||||
*
|
||||
* @return 过滤后的地址对象列表
|
||||
* @return a set of local addresses
|
||||
*/
|
||||
public static Set<InetAddress> getAllAddress() {
|
||||
var networkInterfaces = getAllNetworkInterface();
|
||||
@@ -319,8 +322,8 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得本机的IPv4地址列表<br>
|
||||
* 返回的IP列表有序,按照系统设备顺序
|
||||
* Returns all local IPv4 addresses.<br>
|
||||
* The list is ordered by system device order.
|
||||
*/
|
||||
public static Set<String> localIpv4s() {
|
||||
var localAddressList = getAllAddress().stream()
|
||||
@@ -329,8 +332,8 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得本机的IPv6地址列表<br>
|
||||
* 返回的IP列表有序,按照系统设备顺序
|
||||
* Returns all local IPv6 addresses.<br>
|
||||
* The list is ordered by system device order.
|
||||
*/
|
||||
public static Set<String> localIpv6s() {
|
||||
var localAddressList = getAllAddress().stream()
|
||||
@@ -339,7 +342,7 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 地址列表转换为IP地址列表
|
||||
* Converts a set of {@link InetAddress} objects to a set of IP address strings.
|
||||
*/
|
||||
public static Set<String> toIpList(Set<InetAddress> addressList) {
|
||||
var ipSet = new LinkedHashSet<String>();
|
||||
@@ -350,7 +353,7 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得本机的IP地址列表(包括Ipv4和Ipv6)<br>
|
||||
* Returns all local IP addresses (both IPv4 and IPv6).
|
||||
*/
|
||||
public static Set<String> localIps() {
|
||||
var localAddressList = getAllAddress();
|
||||
@@ -359,13 +362,14 @@ public class NetUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 获取本机网卡IP地址,这个地址为所有网卡中非回路地址的第一个<br>
|
||||
* 如果获取失败调用 {@link InetAddress#getLocalHost()}方法获取。<br>
|
||||
* 此方法不会抛出异常,获取失败将返回<code>null</code><br>
|
||||
* Returns the local machine's primary NIC IP address string —
|
||||
* the first non-loopback address among all network cards.<br>
|
||||
* Falls back to {@link InetAddress#getLocalHost()} on failure.<br>
|
||||
* Never throws; returns an empty string if the address cannot be determined.<br>
|
||||
* <p>
|
||||
* 参考:http://stackoverflow.com/questions/9481865/getting-the-ip-address-of-the-current-machine-using-java
|
||||
* Reference: http://stackoverflow.com/questions/9481865/getting-the-ip-address-of-the-current-machine-using-java
|
||||
*
|
||||
* @return 本机网卡IP地址,获取失败返回空字符串
|
||||
* @return the local NIC IP address string, or an empty string on failure
|
||||
*/
|
||||
public static String getLocalhostStr() {
|
||||
InetAddress localhost = getLocalhost();
|
||||
@@ -376,22 +380,22 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本机网卡IP地址,规则如下:
|
||||
* Returns the local machine's primary NIC {@link InetAddress}, following these rules:
|
||||
*
|
||||
* <pre>
|
||||
* 1. 查找所有网卡地址,必须非回路(loopback)地址、非局域网地址(siteLocal)、IPv4地址
|
||||
* 2. 如果无满足要求的地址,调用 {@link InetAddress#getLocalHost()} 获取地址
|
||||
* 1. Search all NIC addresses; must be non-loopback, site-local, and IPv4.
|
||||
* 2. If no address meets the criteria, fall back to {@link InetAddress#getLocalHost()}.
|
||||
* </pre>
|
||||
*
|
||||
* @return 本机网卡IP地址,获取失败返回<code>null</code>
|
||||
* @return the local NIC address, or {@code null} on failure
|
||||
*/
|
||||
@Nullable
|
||||
public static InetAddress getLocalhost() {
|
||||
var address = getAllAddress().stream()
|
||||
.filter(it -> !it.isLoopbackAddress()
|
||||
// 非地区本地地址,指10.0.0.0 ~ 10.255.255.255、172.16.0.0 ~ 172.31.255.255、192.168.0.0 ~ 192.168.255.255
|
||||
// Site-local: 10.0.0.0–10.255.255.255, 172.16.0.0–172.31.255.255, 192.168.0.0–192.168.255.255
|
||||
&& it.isSiteLocalAddress()
|
||||
// 需为IPV4地址
|
||||
// Must be an IPv4 address
|
||||
&& it instanceof Inet4Address
|
||||
&& !it.getHostAddress().contains(":"))
|
||||
.findFirst();
|
||||
@@ -408,30 +412,30 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得本机MAC地址
|
||||
* Returns the MAC address of the local machine's primary NIC.
|
||||
*
|
||||
* @return 本机MAC地址
|
||||
* @return the MAC address string
|
||||
*/
|
||||
public static String getLocalMacAddress() {
|
||||
return getMacAddress(getLocalhost());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得指定地址信息中的MAC地址,使用分隔符“-”
|
||||
* Returns the MAC address of the NIC bound to the given address, using {@code "-"} as separator.
|
||||
*
|
||||
* @param inetAddress {@link InetAddress}
|
||||
* @return MAC地址,用-分隔
|
||||
* @param inetAddress the {@link InetAddress} whose NIC MAC is needed
|
||||
* @return the MAC address string separated by {@code "-"}
|
||||
*/
|
||||
public static String getMacAddress(InetAddress inetAddress) {
|
||||
return getMacAddress(inetAddress, "-");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得指定地址信息中的MAC地址
|
||||
* Returns the MAC address of the NIC bound to the given address.
|
||||
*
|
||||
* @param inetAddress {@link InetAddress}
|
||||
* @param separator 分隔符,推荐使用“-”或者“:”
|
||||
* @return MAC地址,用-分隔
|
||||
* @param inetAddress the {@link InetAddress} whose NIC MAC is needed
|
||||
* @param separator the separator character; {@code "-"} or {@code ":"} are recommended
|
||||
* @return the MAC address string
|
||||
*/
|
||||
public static String getMacAddress(InetAddress inetAddress, String separator) {
|
||||
AssertionUtils.notNull(inetAddress);
|
||||
@@ -449,7 +453,7 @@ public class NetUtils {
|
||||
if (i != 0) {
|
||||
sb.append(separator);
|
||||
}
|
||||
// 字节转换为整数
|
||||
// Convert each byte to its hex integer representation
|
||||
s = Integer.toHexString(mac[i] & 0xFF);
|
||||
sb.append(s.length() == 1 ? 0 + s : s);
|
||||
}
|
||||
@@ -459,12 +463,12 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用普通Socket发送数据
|
||||
* Sends raw bytes to a remote host using a plain TCP socket.
|
||||
*
|
||||
* @param host Server主机
|
||||
* @param port Server端口
|
||||
* @param data 数据
|
||||
* @throws IOException IO异常
|
||||
* @param host the target server host
|
||||
* @param port the target server port
|
||||
* @param data the data to send
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public static void netCat(String host, int port, byte[] data) throws IOException {
|
||||
OutputStream out = null;
|
||||
@@ -479,7 +483,11 @@ public class NetUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否在一个ip网段下
|
||||
* Checks whether an IP address falls within a given CIDR range.
|
||||
*
|
||||
* @param ip the IP address to test
|
||||
* @param cidr the CIDR notation (e.g. "192.168.1.0/24")
|
||||
* @return {@code true} if the IP is within the CIDR range
|
||||
*/
|
||||
public static boolean isInRange(String ip, String cidr) {
|
||||
if (StringUtils.isBlank(ip)) {
|
||||
|
||||
@@ -56,7 +56,7 @@ public abstract class SessionUtils {
|
||||
} catch (Throwable t) {
|
||||
// do nothing
|
||||
// to avoid: io.netty.channel.unix.Errors$NativeIoException: readAddress(..) failed: Connection reset by peer
|
||||
// 有些情况当建立连接过后迅速关闭,这个时候取remoteAddress会有异常
|
||||
// In some cases when a connection is established and then quickly closed, accessing remoteAddress may throw an exception
|
||||
}
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 单值缓存,会隔一段时间在后台刷新一下缓存
|
||||
* Single-value cache that refreshes itself in the background at a specified interval
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
@@ -44,9 +44,9 @@ public class SingleCache<V> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param refreshDuration 刷新实际那,毫秒
|
||||
* @param supplier 缓存提供者
|
||||
* @return 简单的缓存
|
||||
* @param refreshDuration refresh interval in milliseconds
|
||||
* @param supplier cache value supplier
|
||||
* @return a simple single-value cache
|
||||
*/
|
||||
public static <V> SingleCache<V> build(long refreshDuration, Supplier<V> supplier) {
|
||||
var cache = new SingleCache<V>(refreshDuration, supplier);
|
||||
@@ -64,7 +64,7 @@ public class SingleCache<V> {
|
||||
public V get() {
|
||||
var now = TimeUtils.now();
|
||||
var refreshTime = refreshTimeAtomic.get();
|
||||
// 使用双重检测锁的方式
|
||||
// Double-checked locking approach
|
||||
if (now > refreshTime) {
|
||||
if (refreshTimeAtomic.compareAndSet(refreshTime, now + refreshDuration)) {
|
||||
cache = supplier.get();
|
||||
@@ -76,7 +76,7 @@ public class SingleCache<V> {
|
||||
public V lazyGet() {
|
||||
var now = TimeUtils.now();
|
||||
var refreshTime = refreshTimeAtomic.get();
|
||||
// 使用双重检测锁的方式
|
||||
// Double-checked locking approach
|
||||
if (now > refreshTime) {
|
||||
if (refreshTimeAtomic.compareAndSet(refreshTime, now + refreshDuration)) {
|
||||
lazyRefresh();
|
||||
|
||||
@@ -18,7 +18,7 @@ import com.zfoo.net.packet.common.PairIntLong;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 性能更高,但是可能会浪费一定的内存
|
||||
* A higher-performance tree map implementation, though it may waste some memory.
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
|
||||
@@ -23,9 +23,9 @@ import java.util.Base64;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* AES加密和解密
|
||||
* AES encryption and decryption utilities.
|
||||
* <p>
|
||||
* 默认AES/ECB/PKCS5Padding
|
||||
* Default mode: AES/ECB/PKCS5Padding
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
@@ -35,11 +35,11 @@ public abstract class AesUtils {
|
||||
private static final String KEY_STR = "=jE[`B],YO24Vt+Akh&}D7@s9l1uLKP)";
|
||||
|
||||
/**
|
||||
* 密钥算法
|
||||
* Key algorithm name.
|
||||
*/
|
||||
private static final String ALGORITHM = "AES";
|
||||
/**
|
||||
* 加解密算法/工作模式/填充方式
|
||||
* Cipher transformation: algorithm / operation mode / padding scheme.
|
||||
*/
|
||||
private static final String ALGORITHM_STR = "AES/ECB/PKCS5Padding";
|
||||
|
||||
@@ -79,10 +79,10 @@ public abstract class AesUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 对str进行AES加密
|
||||
* AES-encrypt the given string.
|
||||
*
|
||||
* @param str 需要加密的字符串
|
||||
* @return AES加密后的字符串
|
||||
* @param str the plaintext string to encrypt
|
||||
* @return Base64-encoded AES-encrypted string
|
||||
*/
|
||||
public static String getEncryptString(String str) {
|
||||
try {
|
||||
@@ -104,10 +104,10 @@ public abstract class AesUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 对str进行AES解密
|
||||
* AES-decrypt the given Base64-encoded string.
|
||||
*
|
||||
* @param str 需要解密的字符串
|
||||
* @return AES解密后的字符串
|
||||
* @param str the Base64-encoded AES-encrypted string to decrypt
|
||||
* @return the decrypted plaintext string
|
||||
*/
|
||||
public static String getDecryptString(String str) {
|
||||
try {
|
||||
|
||||
@@ -18,20 +18,24 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* MD5的全称是Message-Digest Algorithm 5,在90年代初由MIT的计算机科学实验室和RSA Data Security Inc发明,经MD2、MD3和MD4发展而来。
|
||||
* MD5 (Message-Digest Algorithm 5) was invented by MIT's Laboratory for Computer Science
|
||||
* and RSA Data Security Inc. in the early 1990s, evolving from MD2, MD3, and MD4.
|
||||
* <p>
|
||||
* MD5将任意长度的“字节串”变换成一个128bit的大整数,并且它是一个不可逆的字符串变换算法。
|
||||
* MD5 converts an arbitrary-length byte sequence into a 128-bit integer using a one-way hash.
|
||||
* <p>
|
||||
* 换句话说就是,即使你看到源程序和算法描述,也无法将一个MD5的值变换回原始的字符串。
|
||||
* In other words, even with access to the source code and algorithm description, it is impossible
|
||||
* to reverse an MD5 hash back to its original input.
|
||||
* <p>
|
||||
* 从数学原理上说,是因为原始的字符串有无穷多个,这有点象不存在反函数的数学函数。
|
||||
* Mathematically this is because there are infinitely many possible input strings — similar
|
||||
* to a mathematical function that has no inverse.
|
||||
* <p>
|
||||
* MD5的典型应用是对一段Message(字节串)产生fingerprint(指纹),以防止被“篡改”。
|
||||
* <p>
|
||||
* 举个例子,你将一段话写在一个叫 readme.txt文件中,并对这个readme.txt产生一个MD5的值并记录在案,然后你可以传播这个文件给别人,
|
||||
* 别人如果修改了文件中的任何内容,你对这个文件重新计算MD5时就会发现。如果再有一个第三方的认证机构,用MD5还可以防止文件作者的“抵赖”,
|
||||
* 这就是所谓的数字签名应用。MD5还广泛用于加密和解密技术上,在很多操作系统中,用户的密码是以MD5值(或类似的其它算法)的方式保存的,
|
||||
* 用户Login的时候,系统是把用户输入的密码计算成MD5值,然后再去和系统中保存的MD5值进行比较,而系统并不“知道”用户的密码是什么。
|
||||
* A typical use of MD5 is to produce a fingerprint for a message (byte string) to detect tampering.
|
||||
* For example, you write text to readme.txt, compute its MD5 and record it, then distribute the file.
|
||||
* If anyone modifies the file, recomputing the MD5 will reveal the change.
|
||||
* With a trusted third party, MD5 can also prevent authors from repudiating their messages — this
|
||||
* is the basis of digital signatures.
|
||||
* MD5 is also widely used in encryption: many OSes store user passwords as MD5 hashes (or similar),
|
||||
* so the system compares the MD5 of the input at login with the stored MD5 — it never "knows" the plaintext password.
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
@@ -40,7 +44,7 @@ public abstract class MD5Utils {
|
||||
private static final String MD5_ALGORITHM = "MD5";
|
||||
|
||||
/**
|
||||
* 16进制字符
|
||||
* Hexadecimal character table.
|
||||
*/
|
||||
private static final char[] HEX_CHARS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
|
||||
|
||||
@@ -53,34 +57,32 @@ public abstract class MD5Utils {
|
||||
return bytesToMD5(StringUtils.bytes(str));
|
||||
}
|
||||
|
||||
// MD5将任意长度的“字节串”变换成一个128bit的大整数
|
||||
// MD5 transforms an arbitrary-length byte sequence into a 128-bit integer
|
||||
public static String bytesToMD5(byte[] bytes) {
|
||||
try {
|
||||
|
||||
var messageDigest = MessageDigest.getInstance(MD5_ALGORITHM);
|
||||
// inputByteArray是输入字符串转换得到的字节数组
|
||||
// Feed the input byte array into the digest
|
||||
messageDigest.update(bytes);
|
||||
|
||||
// 转换并返回结果,也是字节数组,包含16个元素
|
||||
// 字符数组转换成字符串返回,MD5将任意长度的字节数组变换成一个16个字节,128bit的大整数
|
||||
// Compute the digest (16-byte / 128-bit result) and convert to hex string
|
||||
return byteArrayToHex(messageDigest.digest());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("MD加密出现未知异常", e);
|
||||
throw new RuntimeException("Unknown exception during MD5 encryption", e);
|
||||
}
|
||||
}
|
||||
|
||||
//下面这个函数用于将字节数组换成成16进制的字符串
|
||||
// Converts a byte array to a lowercase hexadecimal string
|
||||
private static String byteArrayToHex(byte[] bytes) {
|
||||
// new一个字符数组,这个就是用来组成结果字符串的(解释一下:一个byte是八位二进制,也就是2位十六进制字符(2的8次方等于16的2次方))
|
||||
// Each byte is 8 bits = 2 hex characters (2^8 == 16^2)
|
||||
var resultCharArray = new char[bytes.length * 2];
|
||||
// 遍历字节数组,通过位运算(位运算效率高),转换成字符放到字符数组中去
|
||||
// Use bit shifts (faster than division) to extract high and low nibbles
|
||||
var index = 0;
|
||||
for (var b : bytes) {
|
||||
resultCharArray[index++] = HEX_CHARS[b >>> 4 & 0xf];
|
||||
resultCharArray[index++] = HEX_CHARS[b & 0xf];
|
||||
}
|
||||
|
||||
// 字符数组组合成字符串返回
|
||||
// Assemble the character array into the final string
|
||||
return new String(resultCharArray);
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,11 @@
|
||||
|
||||
package com.zfoo.net.util.security;
|
||||
|
||||
import com.zfoo.protocol.collection.CollectionUtils;
|
||||
import com.zfoo.protocol.util.FileUtils;
|
||||
import com.zfoo.protocol.util.IOUtils;
|
||||
import com.zfoo.protocol.util.StringUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.zip.*;
|
||||
|
||||
/**
|
||||
@@ -29,11 +26,11 @@ public abstract class ZipUtils {
|
||||
|
||||
private static final int BUFFER_SIZE = IOUtils.ONE_BYTE;
|
||||
|
||||
// compression level (0-9),只能是0-9
|
||||
// Compression level (0-9 inclusive)
|
||||
private static final int COMPRESS_LEVEL = 5;
|
||||
|
||||
public static byte[] zip(byte[] bytes) {
|
||||
// deflate [dɪ'fleɪt] v.抽出空气; 缩小
|
||||
// deflate [dɪ'fleɪt] v. to let air out; to reduce in size
|
||||
Deflater deflater = new Deflater(COMPRESS_LEVEL);
|
||||
deflater.setInput(bytes);
|
||||
deflater.finish();
|
||||
@@ -128,16 +125,16 @@ public abstract class ZipUtils {
|
||||
FileUtils.createDirectory(destDirectory);
|
||||
var zipIn = new ZipInputStream(zipFileInputStream);
|
||||
var entry = zipIn.getNextEntry();
|
||||
// 遍历ZIP文件中的所有条目
|
||||
// Iterate over all entries in the ZIP file
|
||||
while (entry != null) {
|
||||
var filePath = destDirectory + File.separator + entry.getName();
|
||||
if (!entry.isDirectory()) {
|
||||
// 如果条目是文件,则解压该文件
|
||||
// Entry is a file — extract it
|
||||
var fileOutputStream = FileUtils.openOutputStream(new File(filePath), false);
|
||||
IOUtils.copy(zipIn, fileOutputStream);
|
||||
IOUtils.closeIO(fileOutputStream);
|
||||
} else {
|
||||
// 如果条目是目录,则创建目录
|
||||
// Entry is a directory — create it
|
||||
FileUtils.createDirectory(filePath);
|
||||
}
|
||||
zipIn.closeEntry();
|
||||
|
||||
@@ -54,7 +54,7 @@ public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuf
|
||||
channel.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
|
||||
@Override
|
||||
public void completed(Integer result, ByteBuffer buffer) {
|
||||
//如果没有发送完成,则继续发送
|
||||
// If the write is not complete, continue sending
|
||||
if (writeBuffer.hasRemaining()) {
|
||||
channel.write(buffer);
|
||||
}
|
||||
|
||||
@@ -14,15 +14,15 @@ public class TcpTest {
|
||||
|
||||
@Test
|
||||
public void serverTest() throws IOException {
|
||||
// 1.穿件服务器,指定端口
|
||||
// 1. Create server socket and bind to port
|
||||
ServerSocket server = null;
|
||||
BufferedWriter bufferedWriter = null;
|
||||
try {
|
||||
server = new ServerSocket(9999);
|
||||
// 2.接受客户端连接,阻塞式
|
||||
// 2. Accept incoming client connection (blocking)
|
||||
Socket socket = server.accept();
|
||||
|
||||
// 3.发送数据+接受数据
|
||||
// 3. Send and receive data
|
||||
String message = "welocme to internet!!!";
|
||||
bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
|
||||
bufferedWriter.write(message);
|
||||
@@ -35,21 +35,21 @@ public class TcpTest {
|
||||
|
||||
@Test
|
||||
public void clientTest() throws IOException {
|
||||
// 1.创建客户端,必须指定服务器+端口,此时就在连接
|
||||
// 1. Create client socket; connecting to server immediately
|
||||
Socket client = null;
|
||||
BufferedReader bufferedReader = null;
|
||||
|
||||
try {
|
||||
// 同一个协议下TCP,端口不能重复,UDP和TCP可以公用一个端口
|
||||
// 端口两个字节,1024一下为系统级别的端口,不能使用
|
||||
// Within the same protocol (TCP), ports must be unique; TCP and UDP can share a port
|
||||
// Ports are 2-byte unsigned; ports below 1024 are system-reserved
|
||||
client = new Socket("localhost", 9999);
|
||||
|
||||
// 2.接受数据
|
||||
// 2. Receive data
|
||||
bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
|
||||
String receiveMes = bufferedReader.readLine();
|
||||
System.out.println(receiveMes);
|
||||
|
||||
// 3.发送数据
|
||||
// 3. Send data
|
||||
client.getOutputStream().write(new String("This is client!").getBytes());
|
||||
client.getOutputStream().flush();
|
||||
} finally {
|
||||
|
||||
@@ -14,23 +14,23 @@ public class UdpTest {
|
||||
|
||||
@Test
|
||||
public void serverTest() throws IOException {
|
||||
// 1.创建服务端+端口
|
||||
// 1. Create server socket and bind to port
|
||||
DatagramSocket server = null;
|
||||
try {
|
||||
server = new DatagramSocket(8000);
|
||||
// 2.准备接收器
|
||||
// 2. Prepare the receive buffer
|
||||
byte[] container = new byte[1024];
|
||||
// 3.封装成包DatagramPacket
|
||||
// 3. Wrap buffer in a DatagramPacket
|
||||
DatagramPacket packet = new DatagramPacket(container,
|
||||
container.length);
|
||||
// 4.接收数据,接收数据可以阻塞
|
||||
// 4. Receive data (blocking call)
|
||||
server.receive(packet);
|
||||
// 分析数据
|
||||
// Process received data
|
||||
byte[] data = packet.getData();
|
||||
int length = packet.getLength();
|
||||
System.out.println(new String(data, 0, length));
|
||||
} finally {
|
||||
// 5.释放资源
|
||||
// 5. Release resources
|
||||
server.close();
|
||||
}
|
||||
|
||||
@@ -38,19 +38,19 @@ public class UdpTest {
|
||||
|
||||
@Test
|
||||
public void clientTest() throws IOException {
|
||||
// 1.创建客户端+端口
|
||||
// 1. Create client socket
|
||||
DatagramSocket client = null;
|
||||
try {
|
||||
client = new DatagramSocket(6666);
|
||||
// 2.准备数据
|
||||
String message = "udp 编程";
|
||||
// 2. Prepare data to send
|
||||
String message = "UDP programming";
|
||||
byte[] data = message.getBytes();
|
||||
// 3.打包DatagramPacket,发送的地点及端口
|
||||
// 3. Pack into a DatagramPacket with destination address and port
|
||||
DatagramPacket packet = new DatagramPacket(data, data.length,
|
||||
new InetSocketAddress("localhost", 8000));
|
||||
// 4.发送
|
||||
// 4. Send the packet
|
||||
client.send(packet);
|
||||
// 5.释放资源
|
||||
// 5. Release resources
|
||||
client.close();
|
||||
} finally {
|
||||
IOUtils.closeIO(client);
|
||||
|
||||
@@ -17,69 +17,69 @@ import java.net.UnknownHostException;
|
||||
@Ignore
|
||||
public class UrlTest {
|
||||
|
||||
// 不包含端口的ip地址
|
||||
// IP address without a port
|
||||
@Test
|
||||
public void inetAddressTest() throws UnknownHostException {
|
||||
// 获取本机的ip
|
||||
// Get the local machine IP
|
||||
InetAddress ipLocal = InetAddress.getLocalHost();
|
||||
System.out.println("本机地址相关信息:");
|
||||
System.out.println("本机计算机名称:" + ipLocal.getHostName());
|
||||
System.out.println("本机ip:" + ipLocal.getHostAddress());
|
||||
System.out.println("Local address info:");
|
||||
System.out.println("Local hostname: " + ipLocal.getHostName());
|
||||
System.out.println("Local IP: " + ipLocal.getHostAddress());
|
||||
|
||||
System.out.println("**************************************************************************");
|
||||
|
||||
// 获取远程ip地址
|
||||
// Get a remote IP address
|
||||
InetAddress ipBaidu = InetAddress.getByName("www.baidu.com");
|
||||
// 如果解析不到name和address都为ip
|
||||
// If resolution fails, both name and address return the raw IP
|
||||
// InetAddress ipBaidu=InetAddress.getByName("115.239.211.112");
|
||||
System.out.println("远程地址相关信息:");
|
||||
System.out.println("远程计算机名称:" + ipBaidu.getHostName());
|
||||
System.out.println("远程ip:" + ipBaidu.getHostAddress());
|
||||
System.out.println("Remote address info:");
|
||||
System.out.println("Remote hostname: " + ipBaidu.getHostName());
|
||||
System.out.println("Remote IP: " + ipBaidu.getHostAddress());
|
||||
}
|
||||
|
||||
// 包含端口的ip地址
|
||||
// IP address with a port
|
||||
@Test
|
||||
public void inetSocketAddressToInetAddressTest() {
|
||||
// localhost=127.0.0.1,本机ip
|
||||
// localhost = 127.0.0.1, loopback
|
||||
InetSocketAddress address = new InetSocketAddress("www.baidu.com", 9999);
|
||||
System.out.println(address.getHostName());
|
||||
System.out.println(address.getPort());
|
||||
|
||||
// InetSocketAddress和InetAddress的相互转换
|
||||
// Conversion between InetSocketAddress and InetAddress
|
||||
InetAddress ip = address.getAddress();
|
||||
System.out.println(ip.getHostName());// 计算机名称
|
||||
System.out.println(ip.getHostAddress());// ip
|
||||
System.out.println(ip.getHostName()); // hostname
|
||||
System.out.println(ip.getHostAddress()); // ip
|
||||
}
|
||||
|
||||
@Test
|
||||
public void urlTest() {
|
||||
// URI统一资源标识符,用来唯一的标识一个资源
|
||||
// URL统一资源定位符,一种具体的URI
|
||||
// 四部分组成:协议 存放资源的主机名称 端口 资源文件名称
|
||||
// URI (Uniform Resource Identifier) uniquely identifies a resource
|
||||
// URL (Uniform Resource Locator) is a specific type of URI
|
||||
// Composed of 4 parts: scheme, host, port, and resource path
|
||||
URL url = null;
|
||||
|
||||
// 读取网页的资源
|
||||
// Read a web resource
|
||||
InputStream inputStream = null;
|
||||
InputStreamReader inputStreamReader = null;
|
||||
BufferedReader bufferedReader = null;
|
||||
try {
|
||||
// 绝对路径
|
||||
// Absolute path
|
||||
url = new URL("https://pic3.zhimg.com/v2-473b053a376f6752da5d5b06a31b7304_b.jpg");
|
||||
System.out.println(url.getProtocol());
|
||||
System.out.println(url.getHost());
|
||||
System.out.println(url.getPort());
|
||||
System.out.println(url.getFile());// 资源
|
||||
System.out.println(url.getPath());// 相对路径
|
||||
System.out.println(url.getRef());// 锚点
|
||||
System.out.println(url.getQuery());// 存在锚点,参数为null;如果不存在锚点,返回正确
|
||||
|
||||
System.out.println(url.getFile()); // resource
|
||||
System.out.println(url.getPath()); // relative path
|
||||
System.out.println(url.getRef()); // anchor/fragment
|
||||
// fragment present => query is null; no fragment => returns the query string
|
||||
System.out.println(url.getQuery());
|
||||
inputStream = url.openStream();
|
||||
inputStreamReader = new InputStreamReader(inputStream, "utf-8");
|
||||
bufferedReader = new BufferedReader(inputStreamReader);
|
||||
|
||||
String message;
|
||||
while ((message = bufferedReader.readLine()) != null) {
|
||||
System.out.println(message);// 输出到控制台
|
||||
System.out.println(message); // print to console
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
@@ -20,7 +20,7 @@ import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* 分隔符通信服务器设计
|
||||
* Delimiter-based echo server example.
|
||||
*
|
||||
* @author SunInsanity
|
||||
* @version 1.0
|
||||
@@ -37,9 +37,9 @@ public class EchoServerTest {
|
||||
}
|
||||
|
||||
public void init() {
|
||||
//配置服务端nio线程组
|
||||
EventLoopGroup bossGroup = new NioEventLoopGroup();//服务端接受客户端连接
|
||||
EventLoopGroup workerGroup = new NioEventLoopGroup();//SocketChannel的网络读写
|
||||
// Configure server-side NIO thread groups
|
||||
EventLoopGroup bossGroup = new NioEventLoopGroup(); // Accept client connections
|
||||
EventLoopGroup workerGroup = new NioEventLoopGroup(); // Handle SocketChannel network I/O
|
||||
try {
|
||||
ServerBootstrap bootstrap = new ServerBootstrap();
|
||||
bootstrap.group(bossGroup, workerGroup)
|
||||
@@ -48,14 +48,14 @@ public class EchoServerTest {
|
||||
.handler(new LoggingHandler(LogLevel.INFO))
|
||||
.childHandler(new ChildChannelHandler());
|
||||
|
||||
//绑定端口,同步等待成功
|
||||
// Bind port and wait synchronously until the bind succeeds
|
||||
ChannelFuture future = bootstrap.bind(9999).sync();
|
||||
//等待服务端监听端口关闭
|
||||
// Wait until the server socket is closed
|
||||
future.channel().closeFuture().sync();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
//优雅的退出,释放线程池资源
|
||||
// Graceful shutdown: release thread pool resources
|
||||
bossGroup.shutdownGracefully();
|
||||
workerGroup.shutdownGracefully();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* 定长通信
|
||||
* Fixed-length framing communication
|
||||
*
|
||||
* @author godotg
|
||||
* @version 1.0
|
||||
@@ -34,23 +34,23 @@ public class EchoServerTest {
|
||||
}
|
||||
|
||||
public void init() {
|
||||
//配置服务端nio线程组
|
||||
EventLoopGroup bossGroup = new NioEventLoopGroup();//服务端接受客户端连接
|
||||
EventLoopGroup workerGroup = new NioEventLoopGroup();//SocketChannel的网络读写
|
||||
// Configure server-side NIO thread groups
|
||||
EventLoopGroup bossGroup = new NioEventLoopGroup(); // Accept client connections
|
||||
EventLoopGroup workerGroup = new NioEventLoopGroup(); // Handle SocketChannel network I/O
|
||||
try {
|
||||
ServerBootstrap bootstrap = new ServerBootstrap();
|
||||
bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
|
||||
.option(ChannelOption.SO_BACKLOG, 100).handler(new LoggingHandler(LogLevel.INFO))
|
||||
.childHandler(new ChildChannelHandler());
|
||||
|
||||
//绑定端口,同步等待成功
|
||||
// Bind port and wait synchronously until the bind succeeds
|
||||
ChannelFuture future = bootstrap.bind(9999).sync();
|
||||
//等待服务端监听端口关闭
|
||||
// Wait until the server socket is closed
|
||||
future.channel().closeFuture().sync();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
//优雅的退出,释放线程池资源
|
||||
// Graceful shutdown: release thread pool resources
|
||||
bossGroup.shutdownGracefully();
|
||||
workerGroup.shutdownGracefully();
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class FileClientTest {
|
||||
protected void initChannel(SocketChannel channel) throws Exception {
|
||||
channel.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
|
||||
channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
|
||||
channel.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));//三者组合起来就是文本换行编码解码器
|
||||
channel.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8)); // Together these three handlers form a text line codec
|
||||
channel.pipeline().addLast(new ClientHandler("rainbow.txt"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* 通过netty的通信,和java原生的RandomAccessFile来实现小文件的传输
|
||||
* Netty small file transfer server using Java's native RandomAccessFile.
|
||||
*
|
||||
* @author godotg
|
||||
* @version 1.0
|
||||
@@ -36,23 +36,23 @@ public class FileServerTest {
|
||||
}
|
||||
|
||||
public void init() {
|
||||
//配置服务端nio线程组
|
||||
EventLoopGroup bossGroup = new NioEventLoopGroup();//服务端接受客户端连接
|
||||
EventLoopGroup workerGroup = new NioEventLoopGroup();//SocketChannel的网络读写
|
||||
// Configure server-side NIO thread groups
|
||||
EventLoopGroup bossGroup = new NioEventLoopGroup(); // Accept client connections
|
||||
EventLoopGroup workerGroup = new NioEventLoopGroup(); // Handle SocketChannel network I/O
|
||||
try {
|
||||
ServerBootstrap bootstrap = new ServerBootstrap();
|
||||
bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
|
||||
.option(ChannelOption.SO_BACKLOG, 100).handler(new LoggingHandler(LogLevel.INFO))
|
||||
.childHandler(new ChildChannelHandler());
|
||||
|
||||
//绑定端口,同步等待成功
|
||||
// Bind port and wait synchronously until the bind succeeds
|
||||
ChannelFuture future = bootstrap.bind(9999).sync();
|
||||
//等待服务端监听端口关闭
|
||||
// Wait until the server socket is closed
|
||||
future.channel().closeFuture().sync();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
//优雅的退出,释放线程池资源
|
||||
// Graceful shutdown: release thread pool resources
|
||||
bossGroup.shutdownGracefully();
|
||||
workerGroup.shutdownGracefully();
|
||||
}
|
||||
@@ -63,7 +63,7 @@ public class FileServerTest {
|
||||
protected void initChannel(SocketChannel channel) throws Exception {
|
||||
channel.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
|
||||
channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
|
||||
channel.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));//三者组合起来就是文本换行编码解码器
|
||||
channel.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8)); // Together these three handlers form a text line codec
|
||||
channel.pipeline().addLast(new FileServerHandler());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* 没有考虑到半包读写的服务器
|
||||
* Simple time server without half-packet read/write handling.
|
||||
*
|
||||
* @author godotg
|
||||
* @version 1.0
|
||||
@@ -29,23 +29,22 @@ public class TimeServerTest {
|
||||
ThreadUtils.sleep(Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
|
||||
public void init() {
|
||||
//配置服务端nio线程组
|
||||
EventLoopGroup bossGroup = new NioEventLoopGroup();//服务端接受客户端连接
|
||||
EventLoopGroup workerGroup = new NioEventLoopGroup();//SocketChannel的网络读写
|
||||
// Configure server-side NIO thread groups
|
||||
EventLoopGroup bossGroup = new NioEventLoopGroup(); // Accept client connections
|
||||
EventLoopGroup workerGroup = new NioEventLoopGroup(); // Handle SocketChannel network I/O
|
||||
try {
|
||||
ServerBootstrap bootstrap = new ServerBootstrap();
|
||||
bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
|
||||
.option(ChannelOption.SO_BACKLOG, 1024).childHandler(new ChildChannelHandler());
|
||||
//绑定端口,同步等待成功
|
||||
// Bind port and wait synchronously until the bind succeeds
|
||||
ChannelFuture future = bootstrap.bind(9999).sync();
|
||||
//等待服务端监听端口关闭
|
||||
// Wait until the server socket is closed
|
||||
future.channel().closeFuture().sync();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
//优雅的退出,释放线程池资源
|
||||
// Graceful shutdown: release thread pool resources
|
||||
bossGroup.shutdownGracefully();
|
||||
workerGroup.shutdownGracefully();
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* 通过换行符解决半包读写的问题
|
||||
* Solves the half-packet problem using line-delimiter framing
|
||||
*
|
||||
* @author godotg
|
||||
* @version 1.0
|
||||
@@ -32,21 +32,21 @@ public class TimeServerTest {
|
||||
}
|
||||
|
||||
public void init() {
|
||||
//配置服务端nio线程组
|
||||
EventLoopGroup bossGroup = new NioEventLoopGroup();//服务端接受客户端连接
|
||||
EventLoopGroup workerGroup = new NioEventLoopGroup();//SocketChannel的网络读写
|
||||
// Configure server-side NIO thread groups
|
||||
EventLoopGroup bossGroup = new NioEventLoopGroup(); // Accept client connections
|
||||
EventLoopGroup workerGroup = new NioEventLoopGroup(); // Handle SocketChannel network I/O
|
||||
try {
|
||||
ServerBootstrap bootstrap = new ServerBootstrap();
|
||||
bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
|
||||
.option(ChannelOption.SO_BACKLOG, 1024).childHandler(new ChildChannelHandler());
|
||||
//绑定端口,同步等待成功
|
||||
// Bind port and wait synchronously until the bind succeeds
|
||||
ChannelFuture future = bootstrap.bind(9999).sync();
|
||||
//等待服务端监听端口关闭
|
||||
// Wait until the server socket is closed
|
||||
future.channel().closeFuture().sync();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
//优雅的退出,释放线程池资源
|
||||
// Graceful shutdown: release thread pool resources
|
||||
bossGroup.shutdownGracefully();
|
||||
workerGroup.shutdownGracefully();
|
||||
}
|
||||
|
||||
@@ -31,21 +31,19 @@ public class UDPClientTest {
|
||||
ThreadUtils.sleep(Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void init() {
|
||||
//配置服务端nio线程组
|
||||
EventLoopGroup group = new NioEventLoopGroup();//服务端接受客户端连接
|
||||
// Configure single-thread NIO event loop
|
||||
EventLoopGroup group = new NioEventLoopGroup(); // Single-thread NIO event loop
|
||||
try {
|
||||
Bootstrap bootstrap = new Bootstrap();
|
||||
bootstrap.group(group).channel(NioDatagramChannel.class)
|
||||
.option(ChannelOption.SO_BROADCAST, true).handler(new UDPClientHandler());
|
||||
//绑定端口,同步等待成功
|
||||
// Bind to a random local port
|
||||
ChannelFuture future = bootstrap.bind(0).sync();
|
||||
//等待服务端监听端口关闭
|
||||
// Get the channel and send data
|
||||
Channel channel = future.channel();
|
||||
|
||||
//向内网的所有机器广播UDP消息
|
||||
// Broadcast a UDP message to all machines on the local network
|
||||
channel.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer("client"
|
||||
, CharsetUtil.UTF_8), new InetSocketAddress("127.0.0.1", 9999))).sync();
|
||||
|
||||
@@ -53,7 +51,7 @@ public class UDPClientTest {
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
//优雅的退出,释放线程池资源
|
||||
// Graceful shutdown: release thread pool resources
|
||||
group.shutdownGracefully();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* UDP通信
|
||||
* UDP communication test
|
||||
*
|
||||
* @author godotg
|
||||
* @version 1.0
|
||||
@@ -28,20 +28,20 @@ public class UDPServerTest {
|
||||
}
|
||||
|
||||
public void init() {
|
||||
//配置服务端nio线程组
|
||||
EventLoopGroup group = new NioEventLoopGroup();//服务端接受客户端连接
|
||||
// Configure single-thread NIO event loop
|
||||
EventLoopGroup group = new NioEventLoopGroup(); // Single-thread NIO event loop
|
||||
try {
|
||||
Bootstrap bootstrap = new Bootstrap();
|
||||
bootstrap.group(group).channel(NioDatagramChannel.class)
|
||||
.option(ChannelOption.SO_BROADCAST, true).handler(new UDPServerHandler());
|
||||
//绑定端口,同步等待成功
|
||||
// Bind port and wait synchronously until the bind succeeds
|
||||
ChannelFuture future = bootstrap.bind(9999).sync();
|
||||
//等待服务端监听端口关闭
|
||||
// Wait until the server socket is closed
|
||||
future.channel().closeFuture().await();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
//优雅的退出,释放线程池资源
|
||||
// Graceful shutdown: release thread pool resources
|
||||
group.shutdownGracefully();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,10 @@ public class FileReaderTest {
|
||||
@Test
|
||||
public void test() throws IOException {
|
||||
RandomAccessFile file = new RandomAccessFile("rainbow.txt", "rw");
|
||||
//FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下
|
||||
FileChannel fileChannel = file.getChannel();//NIO 中都从一个Channel 开始
|
||||
|
||||
// NIO always opens from a Channel; data can be read from Channel into Buffer or written from Buffer to Channel
|
||||
FileChannel fileChannel = file.getChannel();
|
||||
//create buffer with capacity of 48 bytes
|
||||
ByteBuffer buf = ByteBuffer.allocate(1000000000);//数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中
|
||||
ByteBuffer buf = ByteBuffer.allocate(1000000000);
|
||||
//read into buffer
|
||||
int bytesRead = fileChannel.read(buf);
|
||||
|
||||
|
||||
@@ -39,16 +39,16 @@ public class NioClientTest implements Runnable {
|
||||
channel.register(selector, SelectionKey.OP_CONNECT);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
System.out.println("非正常退出");
|
||||
System.out.println("Abnormal exit");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println("客户端启动成功!");
|
||||
System.out.println("Client started successfully!");
|
||||
while (!stop) {
|
||||
//不会阻塞
|
||||
// non-blocking
|
||||
try {
|
||||
selector.select(1001);
|
||||
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
|
||||
@@ -82,10 +82,10 @@ public class NioClientTest implements Runnable {
|
||||
|
||||
public void handleInput(SelectionKey key) throws IOException {
|
||||
if (key.isValid()) {
|
||||
//连接事件发生
|
||||
// Connection event triggered
|
||||
if (key.isConnectable()) {
|
||||
SocketChannel channel = (SocketChannel) key.channel();
|
||||
//如果正在连接,则完成连接
|
||||
// If still connecting, finalize the connection
|
||||
if (channel.finishConnect()) {
|
||||
channel.finishConnect();
|
||||
}
|
||||
@@ -103,15 +103,16 @@ public class NioClientTest implements Runnable {
|
||||
byte[] data = new byte[readBuffer.remaining()];
|
||||
readBuffer.get(data);
|
||||
String message = new String(data, "UTF-8").trim();
|
||||
System.out.println("客户端收到消息:" + message);
|
||||
ByteBuffer writeBuffer = ByteBuffer.wrap(message.getBytes());//将消息返回给客户端
|
||||
System.out.println("Client received message: " + message);
|
||||
// Echo message back to server
|
||||
ByteBuffer writeBuffer = ByteBuffer.wrap(message.getBytes());
|
||||
writeBuffer.flip();
|
||||
channel.write(writeBuffer);
|
||||
} else if (readBytes < 0) {//对链路关闭
|
||||
} else if (readBytes < 0) { // Link closed
|
||||
key.cancel();
|
||||
channel.close();
|
||||
} else {
|
||||
//读到0字节忽略
|
||||
// 0 bytes read, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ public class NioServerTest implements Runnable {
|
||||
ThreadUtils.sleep(Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
//多路复用器,一对多
|
||||
// Multiplexer (Selector): one-to-many
|
||||
private Selector selector;
|
||||
|
||||
//1.获得一个ServerSocket通道,用于监听客户端的连接,它是所有客户端连接的父管道
|
||||
// 1. Obtain a ServerSocketChannel for listening; it is the parent channel for all client connections
|
||||
private ServerSocketChannel serverChannel;
|
||||
|
||||
private volatile boolean stop;
|
||||
@@ -36,34 +36,34 @@ public class NioServerTest implements Runnable {
|
||||
public void init(int port) {
|
||||
try {
|
||||
serverChannel = ServerSocketChannel.open();
|
||||
//2.绑定到port端口,设置通道为非阻塞
|
||||
//serverChannel.socket().bind(new InetSocketAddress(port));
|
||||
// 2. Bind to the port and set the channel to non-blocking mode
|
||||
serverChannel.socket().bind(new InetSocketAddress("127.0.0.1", port));
|
||||
serverChannel.configureBlocking(false);
|
||||
//3.创建多路复用器
|
||||
// 3. Create a Selector (multiplexer)
|
||||
this.selector = Selector.open();
|
||||
//4.多路复用器和通道绑定,并注册绑定事件
|
||||
// 4. Register the channel with the Selector for OP_ACCEPT events
|
||||
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
System.out.println("非正常退出");
|
||||
System.out.println("Abnormal exit");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println("服务器启动成功!");
|
||||
//5.采用轮询的方式监听selector上是否有需要处理的事件(Key)
|
||||
System.out.println("Server started successfully!");
|
||||
// 5. Poll the Selector for ready keys
|
||||
while (!stop) {
|
||||
try {
|
||||
//事件到达selector.open()会返回;否则一直阻塞
|
||||
// Block until at least one event arrives; selector.select() returns when events are ready
|
||||
selector.select();
|
||||
//获得selector中选中的项的迭代器,选中的项为注册的事件
|
||||
// Iterate over the selected keys (ready events)
|
||||
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
SelectionKey key = iterator.next();
|
||||
iterator.remove();//删除已选的key,防止重复处理
|
||||
// Remove processed key to prevent duplicate handling
|
||||
iterator.remove();
|
||||
try {
|
||||
handleInput(key);
|
||||
} catch (Exception e) {
|
||||
@@ -79,7 +79,8 @@ public class NioServerTest implements Runnable {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
//多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
|
||||
// After the Selector is closed, all registered Channels and Pipes are automatically
|
||||
// de-registered and closed, so no need to release them again
|
||||
if (selector != null) {
|
||||
try {
|
||||
selector.close();
|
||||
@@ -91,15 +92,15 @@ public class NioServerTest implements Runnable {
|
||||
|
||||
public void handleInput(SelectionKey key) throws IOException {
|
||||
if (key.isValid()) {
|
||||
if (key.isAcceptable()) { //客户请求连接事件
|
||||
// 6.多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理连接
|
||||
if (key.isAcceptable()) { // Client connection request event
|
||||
// 6. Multiplexer detected new client; complete TCP three-way handshake and establish connection
|
||||
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
|
||||
SocketChannel channel = serverChannel.accept();
|
||||
//7.设置服务端和客户端的连接为非阻塞模式,获得和客户端连接的通道
|
||||
// 7. Set the client channel to non-blocking mode
|
||||
channel.configureBlocking(false);
|
||||
//给客户端发送消息
|
||||
channel.write(ByteBuffer.wrap(new String("Hello client!This is server!").getBytes()));
|
||||
//8.将客户端链路注册到多路复用器上,和客户端连接成功后,为了接受到客户端的消息,需要给通道设置读权限
|
||||
// Send a greeting to the client
|
||||
channel.write(ByteBuffer.wrap(new String("Hello client! This is server!").getBytes()));
|
||||
// 8. Register client channel with the Selector for OP_READ to receive future messages
|
||||
channel.register(selector, SelectionKey.OP_READ);
|
||||
}
|
||||
|
||||
@@ -108,27 +109,27 @@ public class NioServerTest implements Runnable {
|
||||
}
|
||||
|
||||
if (key.isReadable()) {
|
||||
//服务器可读取消息:得到事件发生的Socket通道
|
||||
// Server can read data: retrieve the triggered SocketChannel
|
||||
SocketChannel channel = (SocketChannel) key.channel();
|
||||
//创建读取的缓冲区
|
||||
// Allocate the read buffer
|
||||
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
|
||||
int readBytes = channel.read(readBuffer);
|
||||
if (readBytes > 0) {
|
||||
//接受
|
||||
readBuffer.flip();//limit=position, position=0
|
||||
readBuffer.flip(); // limit=position, position=0
|
||||
byte[] data = new byte[readBuffer.remaining()];
|
||||
readBuffer.get(data);
|
||||
String message = new String(data).trim();
|
||||
System.out.println("服务器收到消息:" + message);
|
||||
System.out.println("Server received message: " + message);
|
||||
|
||||
//发送
|
||||
ByteBuffer writeBuffer = ByteBuffer.wrap(String.format("北京时间:" + new Date().toString()).getBytes());//将消息返回给客户端
|
||||
// Reply with current time
|
||||
ByteBuffer writeBuffer = ByteBuffer.wrap(
|
||||
String.format("Beijing time: " + new Date().toString()).getBytes());
|
||||
channel.write(writeBuffer);
|
||||
} else if (readBytes < 0) {//对链路关闭
|
||||
} else if (readBytes < 0) { // Link closed
|
||||
key.cancel();
|
||||
channel.close();
|
||||
} else {
|
||||
//读到0字节忽略
|
||||
// 0 bytes read, ignore
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,19 +32,19 @@ public class RegistryTest {
|
||||
|
||||
@Test
|
||||
public void registerVoTest() {
|
||||
// 定义2个模块:可以为服务提供者用,也可以为服务消费者用,这个仅仅是模块信息
|
||||
// Define 2 protocol modules that can be used by both providers and consumers (module metadata only)
|
||||
var protocolModule1 = "aaa";
|
||||
var protocolModule2 ="bbb";
|
||||
|
||||
// 服务提供者模块列表和服务提供者配置
|
||||
// 定义2个服务提供者模块
|
||||
// Provider module list and provider configuration
|
||||
// Define 2 service-provider modules
|
||||
var providerModules = List.of(new ProviderModule(protocolModule1, "a"), new ProviderModule(protocolModule2, "b"));
|
||||
// 服务器提供者配置:服务提供者的ip + 服务提供者模块
|
||||
// Provider config: IP address + provider modules
|
||||
var providerConfig = ProviderConfig.valueOf(HostAndPort.valueOf("127.0.0.1", 80).toHostAndPortStr(), providerModules);
|
||||
|
||||
// 服务消费者模块和服务消费者配置(服务消费者模块多一个负载均衡属性)
|
||||
// Consumer module list and consumer config (consumer module has an extra load-balance attribute)
|
||||
var consumerModules = List.of(new ConsumerModule("random", "a"), new ConsumerModule("random", "b"));
|
||||
// 服务消费者配置:这个是没Ip的
|
||||
// Consumer config: no IP (consumers connect out, not listen)
|
||||
var consumerConfig = ConsumerConfig.valueOf(consumerModules);
|
||||
|
||||
var register = Register.valueOf("test", providerConfig, consumerConfig);
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
package com.zfoo.net.core.gateway;
|
||||
|
||||
import com.zfoo.net.NetContext;
|
||||
import com.zfoo.net.anno.PacketReceiver;
|
||||
import com.zfoo.net.packet.gateway.GatewayToProviderRequest;
|
||||
import com.zfoo.net.packet.gateway.GatewayToProviderResponse;
|
||||
import com.zfoo.net.router.attachment.GatewayAttachment;
|
||||
import com.zfoo.net.anno.PacketReceiver;
|
||||
import com.zfoo.net.session.Session;
|
||||
import com.zfoo.protocol.util.JsonUtils;
|
||||
import com.zfoo.protocol.util.StringUtils;
|
||||
@@ -33,7 +33,7 @@ public class GatewayProviderController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(GatewayProviderController.class);
|
||||
|
||||
/**
|
||||
* 注意:这里第2个请求参数以Request结尾,那么第3个参数必须是 GatewayAttachment类型(参加:PacketBus中扫描时的校验)
|
||||
* Note: if the 2nd parameter ends with 'Request', the 3rd parameter must be GatewayAttachment type (see PacketBus scan logic)
|
||||
*/
|
||||
@PacketReceiver
|
||||
public void atGatewayToProviderRequest(Session session, GatewayToProviderRequest request, GatewayAttachment gatewayAttachment) {
|
||||
|
||||
@@ -31,7 +31,7 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 这是客户端连接网关,网关转发到服务提供者的测试用例
|
||||
* Test case: client connects to the gateway, which then forwards requests to the service provider
|
||||
*
|
||||
* @author godotg
|
||||
*/
|
||||
@@ -41,13 +41,10 @@ public class GatewayTest {
|
||||
private static final Logger logger = LoggerFactory.getLogger(GatewayTest.class);
|
||||
|
||||
/**
|
||||
* 启动zookeeper,依次运行下面的测试方法启动:
|
||||
* 1.服务提供者
|
||||
* 2.网关
|
||||
* 3.然后运行clientTest
|
||||
* Start ZooKeeper first, then run the test methods below in order:
|
||||
* 3. Run clientTest
|
||||
* <p>
|
||||
* 消息会通过网关转发到服务提供者
|
||||
*/
|
||||
* Messages are forwarded through the gateway to the service provider.
|
||||
@Test
|
||||
public void startProvider0() {
|
||||
var context = new ClassPathXmlApplicationContext("provider/provider_config.xml");
|
||||
@@ -70,28 +67,28 @@ public class GatewayTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 这是网关
|
||||
* This is the gateway
|
||||
*/
|
||||
@Test
|
||||
public void startGateway() {
|
||||
var context = new ClassPathXmlApplicationContext("gateway/gateway_config.xml");
|
||||
SessionUtilsTest.printSessionInfo();
|
||||
|
||||
// 注意:这里创建的是GatewayServer里面是GatewayRouteHandler(而不是BaseRouteHandler),里面会通过ConsumerSession把消息转发到Provider
|
||||
// Note: GatewayServer uses GatewayRouteHandler (not BaseRouteHandler); it forwards messages to the Provider via a ConsumerSession
|
||||
var gatewayServer = new GatewayServer(HostAndPort.valueOf("127.0.0.1:9000"), null);
|
||||
gatewayServer.start();
|
||||
ThreadUtils.sleep(Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 这里是客户端,客户端先请求数据到到网关(毕竟自己连接的就是网关)
|
||||
* Client side: client first requests data from the gateway.
|
||||
*/
|
||||
@Test
|
||||
public void clientSyncTest() {
|
||||
var context = new ClassPathXmlApplicationContext("gateway/gateway_client_config.xml");
|
||||
SessionUtilsTest.printSessionInfo();
|
||||
|
||||
// 这里的地址是网关的地址
|
||||
// The address here is the gateway address
|
||||
var client = new TcpClient(HostAndPort.valueOf("127.0.0.1:9000"));
|
||||
var session = client.start();
|
||||
|
||||
@@ -106,10 +103,9 @@ public class GatewayTest {
|
||||
var thread = new Thread(() -> {
|
||||
for (int j = 0; j < 10000; j++) {
|
||||
try {
|
||||
// 注意:这里的第2个请求参数是 xxxRequest,不是xxxAsk。 因为这里是网关要将数据转发给Provider的,因此当然不能是xxxAsk这种请求。
|
||||
// 第3个参数argument是null,这样子随机一个服务提供者进行消息处理
|
||||
// Note: 2nd parameter must be xxxRequest (not xxxAsk); 3rd argument null => random provider.
|
||||
var response = NetContext.getRouter().syncAsk(session, request, GatewayToProviderResponse.class, null).packet();
|
||||
logger.info("客户端请求[{}]收到消息[{}]", atomicInteger.incrementAndGet(), JsonUtils.object2String(response));
|
||||
logger.info("client request[{}] received response[{}]", atomicInteger.incrementAndGet(), JsonUtils.object2String(response));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -126,7 +122,7 @@ public class GatewayTest {
|
||||
var context = new ClassPathXmlApplicationContext("gateway/gateway_client_config.xml");
|
||||
SessionUtilsTest.printSessionInfo();
|
||||
|
||||
// 这里的地址是网关的地址
|
||||
// The address here is the gateway address
|
||||
var client = new TcpClient(HostAndPort.valueOf("127.0.0.1:9000"));
|
||||
var session = client.start();
|
||||
|
||||
@@ -141,11 +137,10 @@ public class GatewayTest {
|
||||
var thread = new Thread(() -> {
|
||||
for (int j = 0; j < 1000; j++) {
|
||||
try {
|
||||
// 注意:这里的第2个请求参数是 xxxRequest,不是xxxAsk。 因为这里是网关要将数据转发给Provider的,因此当然不能是xxxAsk这种请求。
|
||||
// 第3个参数argument是null,这样子随机一个服务提供者进行消息处理
|
||||
// Note: 2nd parameter must be xxxRequest (not xxxAsk); 3rd argument null => random provider.
|
||||
NetContext.getRouter().asyncAsk(session, request, GatewayToProviderResponse.class, null)
|
||||
.whenComplete(response -> {
|
||||
logger.info("客户端请求[{}]收到消息[{}]", atomicInteger.incrementAndGet(), JsonUtils.object2String(response));
|
||||
logger.info("client request[{}] received response[{}]", atomicInteger.incrementAndGet(), JsonUtils.object2String(response));
|
||||
});
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
|
||||
@@ -35,7 +35,7 @@ import java.util.function.Function;
|
||||
public class HttpServerTest {
|
||||
|
||||
/**
|
||||
* 访问下面这个网址:
|
||||
* Visit the following URL to test:
|
||||
* <p>
|
||||
* http://127.0.0.1:9000/hello
|
||||
*/
|
||||
@@ -51,7 +51,7 @@ public class HttpServerTest {
|
||||
if (uri.equals("/hello")) {
|
||||
return DecodedPacketInfo.valueOf(HttpHelloRequest.valueOf("other param"), attachment);
|
||||
} else {
|
||||
throw new RunException("未知的http路径[{}]", uri);
|
||||
throw new RunException("Unknown HTTP path[{}]", uri);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -48,8 +48,8 @@ public class JsonWebsocketClientTest {
|
||||
var client = new JsonWebsocketClient(HostAndPort.valueOf("127.0.0.1:9000"), webSocketClientProtocolConfig);
|
||||
var session = client.start();
|
||||
|
||||
// Websocket在建立tcp连接过后不能立刻通讯,还需要使用Http协议去握手升级成Websocket协议,这里等待一秒让websocket内部协议初始化完毕
|
||||
// WebSocket握手是在客户端和服务器之间建立WebSocket连接的过程。它是通过HTTP/HTTPS协议完成的,后续将升级为WebSocket协议。
|
||||
// WebSocket requires an HTTP upgrade handshake before communication can start (even over TCP).
|
||||
// This sleep allows the internal WebSocket protocol initialization to complete.
|
||||
ThreadUtils.sleep(1000);
|
||||
|
||||
var request = new JsonHelloRequest();
|
||||
|
||||
@@ -36,11 +36,11 @@ public class ProviderTest {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProviderTest.class);
|
||||
|
||||
/**
|
||||
* RPC教程:
|
||||
* 1.首先必须保证启动zookeeper
|
||||
* 2.启动服务提供者,startProvider0,startProvider1,startProvider2
|
||||
* 3.启动服务消费者,startSyncRandomConsumer,startAsyncRandomConsumer,startConsistentHashConsumer, startCachedConsistentHashConsumer
|
||||
* 4.每个消费者都是通过不同的策略消费,注意区别
|
||||
* RPC tutorial:
|
||||
* 1. Ensure ZooKeeper is started first
|
||||
* 2. Start the service providers: startProvider0, startProvider1, startProvider2
|
||||
* 3. Start the service consumers: startSyncRandomConsumer, startAsyncRandomConsumer, startConsistentHashConsumer, startCachedConsistentHashConsumer
|
||||
* 4. Each consumer uses a different load-balancing strategy
|
||||
*/
|
||||
@Test
|
||||
public void startProvider0() {
|
||||
@@ -64,7 +64,7 @@ public class ProviderTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机消费,同步请求的方式
|
||||
* Random load-balance, synchronous request mode
|
||||
*/
|
||||
@Test
|
||||
public void startSyncRandomConsumer() throws Exception {
|
||||
@@ -76,14 +76,14 @@ public class ProviderTest {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
ThreadUtils.sleep(1000);
|
||||
var response = NetContext.getConsumer().syncAsk(ask, ProviderMessAnswer.class, null).packet();
|
||||
logger.info("消费者请求[{}]收到消息[{}]", i, JsonUtils.object2String(response));
|
||||
logger.info("consumer request[{}] received response[{}]", i, JsonUtils.object2String(response));
|
||||
}
|
||||
|
||||
ThreadUtils.sleep(Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机消费,异步请求的方式
|
||||
* Random load-balance, asynchronous request mode
|
||||
*/
|
||||
@Test
|
||||
public void startAsyncRandomConsumer() {
|
||||
@@ -97,7 +97,7 @@ public class ProviderTest {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
ThreadUtils.sleep(1000);
|
||||
NetContext.getConsumer().asyncAsk(ask, ProviderMessAnswer.class, null).whenComplete(answer -> {
|
||||
logger.info("消费者请求[{}]收到消息[{}]", atomicInteger.incrementAndGet(), JsonUtils.object2String(answer));
|
||||
logger.info("consumer request[{}] received response[{}]", atomicInteger.incrementAndGet(), JsonUtils.object2String(answer));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ public class ProviderTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 一致性hash算法消费方式
|
||||
* Consistent-hash load-balancing consumer
|
||||
*/
|
||||
@Test
|
||||
public void startConsistentHashConsumer() {
|
||||
@@ -119,7 +119,7 @@ public class ProviderTest {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
ThreadUtils.sleep(1000);
|
||||
NetContext.getConsumer().asyncAsk(ask, ProviderMessAnswer.class, 100).whenComplete(answer -> {
|
||||
logger.info("消费者请求[{}]收到消息[{}]", atomicInteger.incrementAndGet(), JsonUtils.object2String(answer));
|
||||
logger.info("consumer request[{}] received response[{}]", atomicInteger.incrementAndGet(), JsonUtils.object2String(answer));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public class ProviderTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存的一致性hash算法消费方式
|
||||
* Cached consistent-hash load-balancing consumer
|
||||
*/
|
||||
@Test
|
||||
public void startCachedConsistentHashConsumer() {
|
||||
@@ -141,7 +141,7 @@ public class ProviderTest {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
ThreadUtils.sleep(1000);
|
||||
NetContext.getConsumer().asyncAsk(ask, ProviderMessAnswer.class, 100).whenComplete(answer -> {
|
||||
logger.info("消费者请求[{}]收到消息[{}]", atomicInteger.incrementAndGet(), JsonUtils.object2String(answer));
|
||||
logger.info("consumer request[{}] received response[{}]", atomicInteger.incrementAndGet(), JsonUtils.object2String(answer));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public class ProxyServerTest {
|
||||
/**
|
||||
* ReverseProxyServerTest reverse proxy TargetServerTest
|
||||
*
|
||||
* ProxyServer 代理了 TargetServer
|
||||
* ProxyServer acts as a proxy forwarding to TargetServer
|
||||
*/
|
||||
@Test
|
||||
public void startServer() {
|
||||
|
||||
@@ -27,9 +27,9 @@ import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
public class TcpServerTest {
|
||||
|
||||
/**
|
||||
* 单机服务器教程,启动成功过后在com.zfoo.net.core.tcp.client.TcpClientTest中运行startClientTest
|
||||
* Standalone server tutorial. After startup, run startClientTest in TcpClientTest
|
||||
* <p>
|
||||
* startClientTest连接服务器成功过后,会不断的发消息给服务器
|
||||
* Once started, run startClientTest in TcpClientTest to connect.
|
||||
*/
|
||||
@Test
|
||||
public void startServer() {
|
||||
|
||||
@@ -40,7 +40,7 @@ public class BenchmarkAsyncTest {
|
||||
var client = new TcpClient(HostAndPort.valueOf("127.0.0.1:9000"));
|
||||
var session = client.start();
|
||||
|
||||
// 异步请求消息是一起发送过去的,服务器排队处理消息容易导致超时,可以调高Router的DEFAULT_TIMEOUT的超时时间,现在默认是3秒超时
|
||||
// Async requests are sent together; the server queue may cause timeouts. Increase Router's DEFAULT_TIMEOUT if needed.
|
||||
var threadNums = Runtime.getRuntime().availableProcessors();
|
||||
var requestNums = 1000;
|
||||
for (int i = 0; i < threadNums; i++) {
|
||||
@@ -53,11 +53,11 @@ public class BenchmarkAsyncTest {
|
||||
ask.setMessage("Hello, this is async client!");
|
||||
NetContext.getRouter().asyncAsk(session, ask, AsyncMessAnswer.class, null)
|
||||
.whenComplete(answer -> {
|
||||
logger.info("异步请求收到结果[{}]", JsonUtils.object2String(answer));
|
||||
logger.info("async request received result[{}]", JsonUtils.object2String(answer));
|
||||
}
|
||||
);
|
||||
} catch (Exception e) {
|
||||
logger.info("同步请求异常", e);
|
||||
logger.info("sync request exception", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,21 +40,21 @@ public class TcpClientTest {
|
||||
|
||||
var client = new TcpClient(HostAndPort.valueOf("127.0.0.1:9000"));
|
||||
|
||||
// 得到连接到服务器的session
|
||||
// Obtain the session connected to the server
|
||||
var session = client.start();
|
||||
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
var ask = new AsyncMessAsk();
|
||||
ask.setMessage("Hello, this is async client!");
|
||||
|
||||
// Router只是一个接口,它的赋值也是在容器初始化好之后从Spring容器获取到的Router对象
|
||||
// 要发送网络数据,要通过方法asyncAsk,第一个参数就是要发送消息的session
|
||||
// 注意:第3个argument参数是当收到消息后,是要要到哪个线程处理,这是CompletableFuture保证的
|
||||
// 其实这个依然是通过session,然后调用send发送过去的消息
|
||||
// 最终依然是通过BaseRouteHandler得到响应,只不过attachment不为空,会触发CompletableFuture的completable方法,从而得到回调结果
|
||||
// Router is just an interface; the actual implementation is retrieved from the Spring container after initialization
|
||||
// To send network data, call asyncAsk; the first argument is the target session
|
||||
// The 3rd argument specifies which thread handles the response (via CompletableFuture).
|
||||
// Internally this still goes through the session and calls send()
|
||||
// Response is still via BaseRouteHandler; non-null attachment triggers the CompletableFuture callback.
|
||||
NetContext.getRouter().asyncAsk(session, ask, AsyncMessAnswer.class, null)
|
||||
.whenComplete(answer -> {
|
||||
logger.info("异步请求收到结果[{}]", JsonUtils.object2String(answer));
|
||||
logger.info("async request received result[{}]", JsonUtils.object2String(answer));
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -28,23 +28,23 @@ public class TcpServerTest {
|
||||
|
||||
@Test
|
||||
public void startServer() {
|
||||
// 启动流程笔记:
|
||||
// 首先,这里是xml配置,这样子就会解析xml配置中的标签
|
||||
// 当遇到自定义标签后,就会触发NamespaceHandler 和 NetDefinitionParser 的自定义标签解析流程
|
||||
// 接着就会往Spring容器注册相关的各种bean
|
||||
// Startup sequence notes:
|
||||
// First, the XML configuration is loaded and the custom namespace tags are parsed
|
||||
// When a custom tag is encountered, NamespaceHandler and NetDefinitionParser handle the custom tag parsing
|
||||
// The parser then registers the required beans into the Spring container
|
||||
var context = new ClassPathXmlApplicationContext("config.xml");
|
||||
|
||||
// 打印下当前spring管理的bean信息
|
||||
// 可以得出结论:通过解析自定义标签,往spring容器中自己主动注册的几个
|
||||
// 和网络有关的全类名的bean(NetConfig、NetContext、ConfigManager、PacketService、Router、Consumer、SessionManager)
|
||||
// Print the names of all beans currently managed by Spring
|
||||
// Conclusion: by parsing the custom XML tags, the following beans are self-registered:
|
||||
// Network-related beans: NetConfig, NetContext, ConfigManager, PacketService, Router, Consumer, SessionManager
|
||||
String[] beanNames = context.getBeanDefinitionNames();
|
||||
for (String beanName : beanNames) {
|
||||
System.out.println(beanName);
|
||||
}
|
||||
|
||||
// 启动起来一个Tcp服务器
|
||||
// 注意:由于之前往Spring容器中注册的SessionManager,从而可以使用这个bean变量管理所有的连接信息
|
||||
// 这也是spring的好处:很多东西是解耦的,让SessionManager的初始化交给的是spring,而不是自己从main函数开始一个个组件去初始化
|
||||
// Start up a TCP server
|
||||
// Note: the SessionManager bean registered earlier manages all active connection sessions
|
||||
// This is the benefit of Spring: decoupled initialization - SessionManager lifecycle is managed by Spring
|
||||
var server = new TcpServer(HostAndPort.valueOf("127.0.0.1:9000"));
|
||||
server.start();
|
||||
|
||||
|
||||
@@ -51,9 +51,9 @@ public class BenchmarkSyncTest {
|
||||
var ask = new SyncMessAsk();
|
||||
ask.setMessage("Hello, this is sync client!");
|
||||
var answer = NetContext.getRouter().syncAsk(session, ask, SyncMessAnswer.class, null).packet();
|
||||
logger.info("同步请求收到结果[{}]", JsonUtils.object2String(answer));
|
||||
logger.info("sync request received result[{}]", JsonUtils.object2String(answer));
|
||||
} catch (Exception e) {
|
||||
logger.info("同步请求异常", e);
|
||||
logger.info("sync request exception", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ public class TcpClientTest {
|
||||
var ask = new SyncMessAsk();
|
||||
ask.setMessage("Hello, this is sync client!");
|
||||
var answer = NetContext.getRouter().syncAsk(session, ask, SyncMessAnswer.class, null).packet();
|
||||
logger.info("同步请求收到结果[{}]", JsonUtils.object2String(answer));
|
||||
logger.info("sync request received result[{}]", JsonUtils.object2String(answer));
|
||||
}
|
||||
|
||||
ThreadUtils.sleep(Long.MAX_VALUE);
|
||||
|
||||
@@ -27,9 +27,9 @@ import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
public class TcpServerTest {
|
||||
|
||||
/**
|
||||
* 单机服务器教程,启动成功过后在com.zfoo.net.core.tcp.client.TcpClientTest中运行startClientTest
|
||||
* Standalone server tutorial. After startup, run startClientTest in TcpClientTest
|
||||
* <p>
|
||||
* startClientTest连接服务器成功过后,会不断的发消息给服务器
|
||||
* startClientTest in TcpClientTest to connect.
|
||||
*/
|
||||
@Test
|
||||
public void startServer() {
|
||||
|
||||
@@ -48,8 +48,8 @@ public class WebsocketClientTest {
|
||||
var client = new WebsocketClient(HostAndPort.valueOf("127.0.0.1:9000"), webSocketClientProtocolConfig);
|
||||
var session = client.start();
|
||||
|
||||
// Websocket在建立tcp连接过后不能立刻通讯,还需要使用Http协议去握手升级成Websocket协议,这里等待一秒让websocket内部协议初始化完毕
|
||||
// WebSocket握手是在客户端和服务器之间建立WebSocket连接的过程。它是通过HTTP/HTTPS协议完成的,后续将升级为WebSocket协议。
|
||||
// WebSocket requires an HTTP upgrade handshake before communication can start (even over TCP).
|
||||
// This sleep allows the internal WebSocket protocol initialization to complete.
|
||||
ThreadUtils.sleep(1000);
|
||||
|
||||
var request = new WebsocketHelloRequest();
|
||||
|
||||
@@ -63,12 +63,12 @@ public class ProtocolTest {
|
||||
cm.setB(Short.MIN_VALUE);
|
||||
cm.setC(Integer.MIN_VALUE);
|
||||
cm.setD(Long.MIN_VALUE);
|
||||
cm.setF("Hello Jaysunxiao,this is the World!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
|
||||
cm.setF("Hello Jaysunxiaothis is the World!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
|
||||
|
||||
ByteBuf writeBuff = Unpooled.directBuffer();
|
||||
packetService.writeHeaderAndBody(writeBuff, cm, attachment);
|
||||
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// 信息头的长度
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// skip the packet header
|
||||
|
||||
DecodedPacketInfo packetInfo = packetService.read(writeBuff);
|
||||
|
||||
@@ -85,7 +85,7 @@ public class ProtocolTest {
|
||||
ByteBuf writeBuff = Unpooled.buffer();
|
||||
packetService.writeHeaderAndBody(writeBuff, cm, null);
|
||||
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// 信息头的长度
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// skip the packet header
|
||||
|
||||
DecodedPacketInfo packetInfo = packetService.read(writeBuff);
|
||||
|
||||
@@ -104,7 +104,7 @@ public class ProtocolTest {
|
||||
ByteBuf writeBuff = Unpooled.buffer();
|
||||
packetService.writeHeaderAndBody(writeBuff, cm, null);
|
||||
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// 信息头的长度
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// skip the packet header
|
||||
|
||||
DecodedPacketInfo packetInfo = packetService.read(writeBuff);
|
||||
|
||||
@@ -123,7 +123,7 @@ public class ProtocolTest {
|
||||
ByteBuf writeBuff = Unpooled.buffer();
|
||||
packetService.writeHeaderAndBody(writeBuff, cm, null);
|
||||
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// 信息头的长度
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// skip the packet header
|
||||
|
||||
DecodedPacketInfo packetInfo = packetService.read(writeBuff);
|
||||
|
||||
@@ -158,7 +158,7 @@ public class ProtocolTest {
|
||||
ByteBuf writeBuff = Unpooled.buffer();
|
||||
packetService.writeHeaderAndBody(writeBuff, cm, null);
|
||||
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// 信息头的长度
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// skip the packet header
|
||||
|
||||
DecodedPacketInfo packetInfo = packetService.read(writeBuff);
|
||||
|
||||
@@ -188,7 +188,7 @@ public class ProtocolTest {
|
||||
ByteBuf writeBuff = Unpooled.buffer();
|
||||
packetService.writeHeaderAndBody(writeBuff, cm, null);
|
||||
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// 信息头的长度
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// skip the packet header
|
||||
|
||||
DecodedPacketInfo packetInfo = packetService.read(writeBuff);
|
||||
|
||||
@@ -207,7 +207,7 @@ public class ProtocolTest {
|
||||
ByteBuf writeBuff = Unpooled.buffer();
|
||||
packetService.writeHeaderAndBody(writeBuff, cm, null);
|
||||
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// 信息头的长度
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// skip the packet header
|
||||
|
||||
DecodedPacketInfo packetInfo = packetService.read(writeBuff);
|
||||
|
||||
@@ -235,7 +235,7 @@ public class ProtocolTest {
|
||||
ByteBuf writeBuff = Unpooled.buffer();
|
||||
packetService.writeHeaderAndBody(writeBuff, cm, null);
|
||||
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// 信息头的长度
|
||||
writeBuff.readerIndex(PacketService.PACKET_HEAD_LENGTH);// skip the packet header
|
||||
|
||||
DecodedPacketInfo packetInfo = packetService.read(writeBuff);
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ public class CpuCacheTesting {
|
||||
competeTaskCount++;
|
||||
}
|
||||
}
|
||||
System.out.println(StringUtils.format("cpu缓存友好写法胜出{} 不友好写法胜出{}", hashTaskCount, competeTaskCount));
|
||||
System.out.println(StringUtils.format("cache-friendly wins:{} cache-unfriendly wins:{}", hashTaskCount, competeTaskCount));
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ public class CpuCacheTesting {
|
||||
countdown.await();
|
||||
long endTime = System.currentTimeMillis();
|
||||
long cost = endTime - startTime;
|
||||
System.out.println(StringUtils.format("cpu缓存友好的任务调度 -> {}", cost));
|
||||
System.out.println(StringUtils.format("cache-friendly task dispatch -> {}ms", cost));
|
||||
return cost;
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ public class CpuCacheTesting {
|
||||
countdown.await();
|
||||
long endTime = System.currentTimeMillis();
|
||||
long cost = endTime - startTime;
|
||||
System.out.println(StringUtils.format("cpu缓存不友好的任务调度 -> {}", cost));
|
||||
System.out.println(StringUtils.format("cache-unfriendly task dispatch -> {}ms", cost));
|
||||
return cost;
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ import java.util.TreeMap;
|
||||
@Ignore
|
||||
public class ConsistentHashTest {
|
||||
|
||||
//待添加入Hash环的服务器列表
|
||||
// List of servers to be added to the hash ring
|
||||
private static final List<Pair<String, String>> servers = List.of(new Pair<>("192.168.0.0:111", "192.168.0.0:111")
|
||||
, new Pair<>("192.168.0.1:111", "192.168.0.1:111"), new Pair<>("192.168.0.2:111", "192.168.0.2:111"));
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ public class SessionUtilsTest {
|
||||
}
|
||||
});
|
||||
|
||||
builder.append(StringUtils.format("serverSession count:[{}]", NetContext.getSessionManager().serverSessionSize()));
|
||||
builder.append(StringUtils.format("serverSession count[{}]", NetContext.getSessionManager().serverSessionSize()));
|
||||
builder.append(FileUtils.LS);
|
||||
NetContext.getSessionManager().forEachServerSession(new Consumer<Session>() {
|
||||
@Override
|
||||
|
||||
@@ -20,22 +20,22 @@ import org.junit.Test;
|
||||
*/
|
||||
public class ZipTest {
|
||||
|
||||
// Zip算法压缩测试
|
||||
// ZIP compression algorithm test
|
||||
@Test
|
||||
public void test() {
|
||||
String str = "ZIP,是一个文件的压缩的算法,原名Deflate(真空),发明者为菲利普·卡兹(Phil Katz))," +
|
||||
"他于1989年1月公布了该格式的资料。ZIP通常使用后缀名“.zip”,它的MIME格式为 application/zip 。" +
|
||||
"目前,ZIP格式属于几种主流的压缩格式之一,其竞争者包括RAR格式以及开放源码的7-Zip格式。" +
|
||||
"从性能上比较,RAR格式较ZIP格式压缩率较高,但是它的压缩时间远远高于Zip。" +
|
||||
"而7-Zip(7z)由于提供了免费的压缩工具而逐渐在更多的领域得到应用。";
|
||||
String str = "ZIP is a file compression algorithm, originally named Deflate, invented by Phil Katz." +
|
||||
"He published the format specification in December 1989. ZIP files typically use the suffix '.zip'." +
|
||||
"Currently, ZIP is one of the mainstream compression formats, competing with RAR and the open-source 7-Zip format." +
|
||||
"RAR has a higher compression ratio than ZIP, but takes much longer to compress." +
|
||||
"7-Zip (7z) is increasingly adopted due to its free compression tools.";
|
||||
|
||||
byte[] bytes = str.getBytes();
|
||||
|
||||
// 压缩前数组长度
|
||||
// Length before compression
|
||||
Assert.assertEquals(bytes.length, 555);
|
||||
|
||||
bytes = ZipUtils.zip(bytes);
|
||||
// 压缩后数组长度
|
||||
// Length after compression
|
||||
Assert.assertEquals(bytes.length, 438);
|
||||
|
||||
bytes = ZipUtils.unZip(bytes);
|
||||
|
||||
@@ -16,7 +16,7 @@ public class CreateNodeASyncTest {
|
||||
|
||||
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
|
||||
|
||||
// 异步创建一个节点
|
||||
// Async node creation
|
||||
@Test
|
||||
public void test() throws IOException, InterruptedException {
|
||||
ZooKeeper zookeeper = new ZooKeeper("localhost:2181", 5000, new CreateNodeASyncWatcher());
|
||||
@@ -27,7 +27,7 @@ public class CreateNodeASyncTest {
|
||||
|
||||
String content = "Hello Zookeeper!";
|
||||
|
||||
// 异步创建,会回调processResult()
|
||||
// Async creation: processResult() is invoked on completion
|
||||
zookeeper.create("/node_test", content.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT, new StringCallback(), "hello");
|
||||
|
||||
Thread.sleep(Integer.MAX_VALUE);
|
||||
@@ -36,7 +36,7 @@ public class CreateNodeASyncTest {
|
||||
static class CreateNodeASyncWatcher implements Watcher {
|
||||
@Override
|
||||
public void process(WatchedEvent event) {
|
||||
System.out.println("接受事件" + event);
|
||||
System.out.println("Received event: " + event);
|
||||
if (event.getState() == KeeperState.SyncConnected) {
|
||||
if (event.getType() == EventType.None && null == event.getPath()) {
|
||||
connectedSemaphore.countDown();
|
||||
|
||||
@@ -41,7 +41,7 @@ public class CreateNodeSyncAuthTest {
|
||||
|
||||
@Override
|
||||
public void process(WatchedEvent event) {
|
||||
System.out.println("收到事件:" + event);
|
||||
System.out.println("Received event: " + event);
|
||||
if (event.getState() == KeeperState.SyncConnected) {
|
||||
if (!somethingDone && event.getType() == EventType.None && null == event.getPath()) {
|
||||
doSomething();
|
||||
@@ -50,21 +50,21 @@ public class CreateNodeSyncAuthTest {
|
||||
}
|
||||
|
||||
/*
|
||||
* 权限模式(scheme): ip, digest
|
||||
* 授权对象(ID)
|
||||
* ip权限模式: 具体的ip地址
|
||||
* digest权限模式: username:Base64(SHA-1(username:password))
|
||||
* 权限(permission): create(C), DELETE(D),READ(R), WRITE(W), ADMIN(A)
|
||||
* 注:单个权限,完全权限,复合权限
|
||||
* Permission scheme: ip, digest
|
||||
* Authorization ID
|
||||
* ip: ip
|
||||
* digest: username:Base64(SHA-1(username:password))
|
||||
* Permission flags: CREATE(C), DELETE(D), READ(R), WRITE(W), ADMIN(A)
|
||||
*
|
||||
*
|
||||
* 权限组合: scheme + ID + permission
|
||||
* ACL entry = scheme + ID + permission
|
||||
* */
|
||||
private void doSomething() {
|
||||
try {
|
||||
// ACL access control list
|
||||
ACL aclIp = new ACL(Perms.READ, new Id("ip", "192.168.1.105"));
|
||||
|
||||
ACL aclDigest = new ACL(Perms.READ | Perms.WRITE, new Id("digest", DigestAuthenticationProvider.generateDigest("godotg:123456")));// 用户名+密码
|
||||
ACL aclDigest = new ACL(Perms.READ | Perms.WRITE, new Id("digest", DigestAuthenticationProvider.generateDigest("godotg:123456")));// username and password
|
||||
|
||||
ArrayList<ACL> acls = new ArrayList<>();
|
||||
acls.add(aclDigest);
|
||||
@@ -76,7 +76,7 @@ public class CreateNodeSyncAuthTest {
|
||||
}
|
||||
|
||||
try {
|
||||
// 没有取得权限之前,不能访问/node_test
|
||||
// username and passwordnode_test
|
||||
zookeeper.getData("/node_test", false, stat);
|
||||
somethingDone = true;
|
||||
} catch (Exception e) {
|
||||
@@ -84,7 +84,7 @@ public class CreateNodeSyncAuthTest {
|
||||
}
|
||||
|
||||
try {
|
||||
// 下面两句设置ACL的哪些author可以访问
|
||||
// The following lines configure which authorized users can access this node
|
||||
zookeeper.addAuthInfo("digest", "godotg:123456".getBytes());
|
||||
zookeeper.getData("/node_test", false, stat);
|
||||
somethingDone = true;
|
||||
|
||||
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user