[经验] Java 使用 netty 框架, 向 Unity 客户端的 C# 实现通信[2]
在前一篇文章中, 我们实现了从Java netty 服务端到 unity 客户端的通讯, 但是在过程中也发现有一些问题是博主苦苦无法解决的, 但是还好终于有些问题还是被我找刀方法解决了, 现在把这些解决方案提出来, 虽然是很简陋的方法, 但是应该可以有一些帮助, 然后呢, 如果大家有更好的解决方案也欢迎留言,
ok 话不多说, 开始代码的表演
首先呢, 先来写一个缓存的部分
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantReadWriteLock; import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap; import com.googlecode.concurrentlinkedhashmap.Weighers; /* *@Description //TODO 实体缓存对象$ *@Author 吾王剑锋所指 吾等心之所向 *@Date 2019/8/22 10:39 */ public abstract class EntityCachedObject<T, PK extends Serializable> { private static final Logger LOGGER = LoggerFactory.getLogger(EntityCachedObject.class); private static final ConcurrentLinkedHashMap.Builder<String, ReentrantReadWriteLock.WriteLock> BUILDER = new ConcurrentLinkedHashMap.Builder<>(); private static final ConcurrentLinkedHashMap<String, ReentrantReadWriteLock.WriteLock> LOCK_MAP = BUILDER.maximumWeightedCapacity(100).weigher(Weighers.singleton()).build(); private static final ConcurrentLinkedHashMap.Builder<String, Cache> ENTITY_CACHE_BUILDER = new ConcurrentLinkedHashMap.Builder<>(); private static final ConcurrentLinkedHashMap<String, Cache> ENTITY_CACHE_MAP = ENTITY_CACHE_BUILDER.maximumWeightedCapacity(20000).weigher(Weighers.singleton()).build(); private static final ConcurrentLinkedHashMap.Builder<String, ConcurrentHashMap<String, Cache>> COMMON_CACHE_BUILDER = new ConcurrentLinkedHashMap.Builder<>(); private static final ConcurrentLinkedHashMap<String, ConcurrentHashMap<String, Cache>> COMMON_CACHE_MAP = COMMON_CACHE_BUILDER.maximumWeightedCapacity(5000).weigher(Weighers.singleton()).build(); /** * 取得当前组的 HashKey * */ protected abstract String getHashKey(); /** * 从数据库获得数据对象 * * @param id 获得数据ID * @return Object 数据库的缓存对象 * */ protected abstract T getEntityFromDB(PK id); protected List<T> getEntityFromIdList(List<PK> idList, Class<T> clazz) { List<T> entityList = new ArrayList<>(idList == null ? 0 : idList.size()); if(idList != null && !idList.isEmpty()) { for (PK id : idList) { T entityFromCache = this.get(id, clazz); if (entityFromCache != null) { entityList.add(entityFromCache); } } } return entityList; } /** * 获得通用缓存 * * @param subKey * @return */ protected Object getFromCommonCache(String subKey) { Cache cache = null; String hashKey = this.getHashKey(); ConcurrentHashMap<String, Cache> cacheMap = COMMON_CACHE_MAP.get(hashKey); if(cacheMap != null && !cacheMap.isEmpty()) { cache = cacheMap.get(subKey); } return cache == null || cache.isTimeout() ? null : cache.getValue(); } /** * 把存储信息加到缓存中 * * @param subKey * @param value */ protected void putToCommonHashCache(String subKey, Object value) { String hashKey = this.getHashKey(); Map<String, Cache> cacheMap = COMMON_CACHE_MAP.get(hashKey); if(cacheMap == null) { COMMON_CACHE_MAP.put(hashKey, new ConcurrentHashMap<>()); cacheMap = COMMON_CACHE_MAP.get(hashKey); } cacheMap.put(subKey, Cache.valueOf(value)); } /** * 移除主KEY * * @param hashkey 主KEY */ protected void removeFromCommonHashCache(String hashkey) { COMMON_CACHE_MAP.remove(hashkey); } /** * 移除SUBKEY * * @param hashKey 主KEY * @param subkey 子KEY */ protected void removeFromCommonSubCache(String hashKey, String subkey) { Map<String, Cache> hashMap = COMMON_CACHE_MAP.get(hashKey); if(hashMap != null) hashMap.remove(subkey); } /** * 从缓存中移除实体对象 * * @param id * @param clazz */ protected void removeEntityFromCache(PK id, Class<T> clazz) { ENTITY_CACHE_MAP.remove(this.getEntityIdKey(id, clazz)); } /** * 取得对象的读写锁 * * @param clazz 类对象 * @return WriteLock 写锁 */ private ReentrantReadWriteLock.WriteLock getWriteLock(Class<T> clazz) { String clazzNameKey = clazz.getName(); ReentrantReadWriteLock.WriteLock writeLock = LOCK_MAP.get(clazzNameKey); if(writeLock == null) { LOCK_MAP.putIfAbsent(clazzNameKey, new ReentrantReadWriteLock().writeLock()); writeLock = LOCK_MAP.get(clazzNameKey); } return writeLock; } /** * 取得实体IDKEY * * @param id * @param clazz * @param <T> * @return */ private <T, PK> String getEntityIdKey(PK id, Class<T> clazz) { return new StringBuilder().append(clazz.getName()).append("_").append(id).toString(); } /** * 从缓存获得实体对象 * * @param id * @param clazz * @return */ protected T get(PK id, Class<T> clazz) { String entityIdKey = this.getEntityIdKey(id, clazz); Cache cachedVO = ENTITY_CACHE_MAP.get(entityIdKey); if(cachedVO != null && !cachedVO.isTimeout()) { return (T) cachedVO.getValue(); } ReentrantReadWriteLock.WriteLock writeLock = this.getWriteLock(clazz); try { writeLock.lock(); cachedVO = ENTITY_CACHE_MAP.get(entityIdKey); if(cachedVO == null || cachedVO.isTimeout()) { Object entityFromDB = this.getEntityFromDB(id); ENTITY_CACHE_MAP.put(entityIdKey, Cache.valueOf(entityFromDB)); return (T) entityFromDB; } } catch (Exception e) { LOGGER.error("{}", e); } finally { writeLock.unlock(); } return null; } }
然后为上面的抽象类创建一个缓存对象
/* *@Description //TODO 缓存对象$ *@Author 吾王剑锋所指 吾等心之所向 *@Date 2019/8/22 10:47 */ public class Cache { private static final Integer TTL_NULL = 60 * 1000; private static final Integer TTL_SIMPLE = 24 * 60 *TTL_NULL; private Integer ttl; private Object value; private Long startTime; public Integer getTtl() { return ttl; } public Object getValue() { if(this.isTimeout()){ this.value = null; } return this.value; } public Long getStartTime() { return startTime; } public Boolean isTimeout(){ return System.currentTimeMillis() >= startTime + ttl; } public void updateCache(Object value){ this.value = value; this.startTime = System.currentTimeMillis(); this.ttl = value == null ? TTL_NULL : TTL_SIMPLE; } public static Cache valueOf(Object value){ Cache cache = new Cache(); cache.updateCache(value); return cache; } }
再来一个类, 用于对 ctx 的缓存, 当然不一定只是能缓存CTX , 很多数据只要你想缓存的就可以丢进去
import cn.gzserver.common.Result; import cn.gzserver.operate.vo.ItemVO; import io.netty.channel.ChannelHandlerContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; /* *@Description //TODO 缓存帮助类$ *@Author 吾王剑锋所指 吾等心之所向 *@Date 2019/8/30 11:11 */ public class EntityCacheHelp { private static final Logger LOGGER = LoggerFactory.getLogger(EntityCacheHelp.class);
public static Cache<Integer, ChannelHandlerContext> channelIdCache = CacheBuilder .newBuilder() .maximumSize(99999) .expireAfterAccess(365, TimeUnit.DAYS) .build(); public static boolean arrayEquals(String[] a,String[] b){ return Arrays.equals(a, b); } public static String[] getItemVosByIndicex(List<ItemVO> itemVos){ Set<String> set = new HashSet<>(); return set.toArray(new String[set.size()]); } }
然后这个缓存就算是完成了, 现在我们去netty服务启动的地方, 将 ctx 对象添加到缓存中
@Override public void channelActive(ChannelHandlerContext ctx) throws Exception { LOGGER.info("RemoteAddress"+ ctx.channel().remoteAddress() + " active !"); LOGGER.info("msg send active !"+ctx.channel().writeAndFlush("123456")); ctx.writeAndFlush("啦啦啦!"); EntityCacheHelp.channelIdCache.put(56193,ctx); super.channelActive(ctx); }
这样, 这个 ctx 就被加到缓存中了, 大家可以看见在 Map 类型的缓存中, ctx 只是作为 value 的存在, 而前面的那段数字 就是key , 这个key 可以把它理解为客户端连接上服务端后的唯一标识, 由客户端在连接成功的时候提供, 这样方便在服务端向客户端发送信息的时候好准确的找到相应客户端的ID
当我们要主动发送信息的时候, 就在方法里使用
ChannelHandlerContext ctx = EntityCacheHelp.channelIdCache.getIfPresent(clientId); ctx.writeAndFlush("jsonObject");
这样就可以了, 当 clientId = 56139 的时候, 就能拿到想要的 ctx, 这样的话, 我们的 netty 服务端也就活过来了, 而不是像网上的大多数教程那样, 只能在服务启动的时候, 客户端和服务端使用固定的信息进行通信, 只要能在 netty 服务启动类之外的地方拿到 ctx, 我们就能为所欲为了哈哈哈哈哈哈哈哈哈
当然, 这个解决方案还是存在一些问题
1: 缓存的时间是有限的, 但是netty作为服务端, 而客户端又是固定的情况下, 一般连接上了就不会断开了, 然后通过用户的手机程序向服务端发送信息, 服务端再把信息发送到客户端, 所以这个方法只能在测试的情况下使用, 而在生产环境中, 当一个服务器控制全国各地几百几千台客户端机器的时候, 是不可能重启服务器让客户端从新连入的
2: 我的解决方案的话是有两种, 一种是通过 redis 将 ctx 缓存起来, 或者永久性的缓存, 要不就直接保存到数据服务器, 也就是数据库中, 当用户需要给哪个客户端发送信息的时候, 服务端直接拿着用户给的 clientId 去数据库服务器查找, 找到 ctx 之后再发送给客户端,
3: 这样的话就还有一个疑惑, ctx 每一次好像都不一样, 那么, 如果网络条件发送变化, 这种情况又应该怎么解决呢?
我现在还没有想到, 等我想到了就一定会分享给大家, 如果有哪位大佬了解这种情况的解决方案, 也望不吝赐教.
一直以来都是我去看别人的博客别人的教程, 我终于也可以自己写博客了, 做为一名程序员, 我很自豪, 也很骄傲, 我愿意为这个开源精神奉献我所有的想法和经验!