生活技能、登录开发开发积累
目录
生活技能
1.Collections.sort(list)方法时,如果list是不可修改的,将报不可操作错误
public static void main( String[] args )
{
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
//将list变为不可修改
List<Integer> unModifiableList = Collections.unmodifiableList(list);
test(unModifiableList);
}
private static void test(List<Integer> list)
{
//对不可修改的list进行排序,将报错
Collections.sort(list);
for (int v : list)
{
System.out.println(v);
}
}
结果:
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableList.sort(Collections.java:1331)
at java.util.Collections.sort(Collections.java:141)
at com.lzb.App.test(App.java:38)
at com.lzb.App.main(App.java:32)
游戏开发:
// 生活技能合成操作 (烹饪、制药、熔矿)
message CSAbilityComposeOperate {
required int32 abilityId = 1; // 生活技能id
optional int32 composeId = 2; // 配方id(制药、熔矿、烹饪)
required int32 composeNum = 3; // 合成数量
required int32 useItemType = 4; // 使用材料类型 1 先绑定后非绑 2 只绑定 3 只非绑
required int32 panEditorId = 5; // 烹饪锅的id
optional bool useOptionalItem = 6; // 是否使用高级材料
repeated int32 cookItems = 7; // 烹饪消耗食材列表
}
用protobuf时,例如repeated int32 cookItems = 7,服务器接受时默认是一个不可修改的list, 所以我们可以先将list转成可修改的list,然后再调用Collections.sort()给list排序,就不会报错了。
//转为可修改的list,再调用sort方法
List<Integer> modifiableList = new ArrayList<>(unmodifiableList);
Collections.sort(modifiableList);
2.jdk原生HashMap在get的时候,如果键值对没匹配到,那么返回的是null值
HashMap<String, Integer> map1 = new HashMap<>();
map1.put("lzb", 100);
HashMap<Integer, Integer> map2 = new HashMap<>();
map2.put(1, 1);
System.out.println(map1.get("swf"));
System.out.println(map2.get(2));
结果:
null
null
项目使用的trov4j里的 TIntIntHashMap如果key值没匹配上,那么返回是0
TIntIntHashMap map3 = new TIntIntHashMap();
map3.put(1, 1);
System.out.println(map3.get(2));
结果:
0
3.多个id组合成匹配唯一id
实际场景:烹饪配方id由策划可配的食材道具id组成(数量为1~5),不同的道具id组合成唯一的一个配方id。 前端发来乱序的食材道具id列表,我们去匹配是否有符合的烹饪配方id,这个场景如何做到性能较好的实现?
实现方案:
初始化处理数据表数据时,将每个烹饪配方需要的食材id列表按从小到大先排序,然后将食材id列表拼接成一个字符串,字符串作为HashMap的key,value则是对应的烹饪配方id。
前端发来乱序的道具食材id列表,同样先将食材id进行从小到大排序,然后拼接成字符串,以这个字符串,去HashMap里匹配是否有烹饪配方id。
后来发现,字符串进行哈希时,每次都需要变量字符串,如果字符串过长,那么效率哈希效率低下。
改为,采用简版AC匹配算法,也就是多级索引:
代码如下:
/**
* 生活技能配方简版AC匹配算法
* @author lizhibiao
* @date 2019/11/5 20:48
*/
public class AbilityFormulaMatchFilter
{
/**
* DFA入口
*/
private final AbilityFormulaMatchFilter.DFANode dfaEntrance;
public AbilityFormulaMatchFilter(TIntObjectHashMap<TIntArrayList> formulaDataMap)
{
dfaEntrance = new DFANode();
clear();
if (null != formulaDataMap)
{
TIntObjectIterator<TIntArrayList> iterator = formulaDataMap.iterator();
while (iterator.hasNext())
{
iterator.advance();
//配方id
int formulaId = iterator.key();
TIntArrayList itemIdList = iterator.value();
DFANode currentDFANode = dfaEntrance;
int size = itemIdList.size();
for (int i = 0; i < size; i++)
{
//道具id
int itemId = itemIdList.get(i);
DFANode nextNode = currentDFANode.dfaTransition.get(itemId);
if (null == nextNode)
{
nextNode = new DFANode();
currentDFANode.dfaTransition.put(itemId, nextNode);
//注意这里是下一节点层数加1
nextNode.level = currentDFANode.level + 1;
}
currentDFANode = nextNode;
}
//如果当前节点不等于DFA入口,那么说明是终节点,赋值配方id
if (currentDFANode != dfaEntrance)
{
currentDFANode.formulaId = formulaId;
}
}
}
}
/**
* 根据道具id列表匹配烹饪配方
* @param orderItemIdList 有顺序的道具id列表
* @return 小于等于0未匹配, 大于0才是匹配成功返回配方id
*/
public int matchFormulaId(List<Integer> orderItemIdList)
{
if (CollectionUtils.isBlank(orderItemIdList))
{
return -1;
}
if (null == dfaEntrance)
{
return -1;
}
//入口节点层数为0
DFANode currentDFANode = dfaEntrance;
int size = orderItemIdList.size();
for (int i = 0; i < size; i++)
{
int itemId = orderItemIdList.get(i);
DFANode nextDFANode = currentDFANode.dfaTransition.get(itemId);
if (null != nextDFANode)
{
//当前节点等于下一节点,继续往下匹配
currentDFANode = nextDFANode;
//如果一直匹配,那么下一节点的层数和列表长度相等表明迭代完毕返回烹饪配方id
if (size == currentDFANode.level)
{
return currentDFANode.formulaId;
}
}
else
{
//未匹配
return -1;
}
}
return -1;
}
/**
* DFA节点.
*/
private static class DFANode
{
/**
* 配方id
*/
private int formulaId;
/**
* key道具id, value为DFA节点
*/
private final TIntObjectHashMap<AbilityFormulaMatchFilter.DFANode> dfaTransition;
/**
* 节点层数
*/
private int level;
public DFANode()
{
this.dfaTransition = new TIntObjectHashMap<>();
formulaId = 0;
level = 0;
}
}
/**
* 初始化时先调用此函数清理
*/
private void clear()
{
// 清理入口
dfaEntrance.dfaTransition.clear();
}
}
4.采集读条广播动作增加玩家朝向
实际场景:当玩家开始读条进行采集时,玩家朝向可能是背对采集物的,所以需要服务调整好朝向并广播给周围玩家。 那么我们需要根据当前玩家坐标点(x, z)和采集物坐标点(x, z),准确计算朝向。那么该如何计算朝向?
/** 面向某点 */
public void faceTo(int x,int z)
{
if(this.x == x && this.z == z)
{
return;
}
setRotation(MapUtils.rotationBetweenPos(this.x,this.z, x, z));
}
分析:调用MapUtils.rotationBetweenPos(this.x,this.z, x, z)计算出朝向,直接set到当前场景对象
/** 返回点1对于点0的角度 */
public static int rotationBetweenPos(float x0, float y0, float x1, float y1)
{
double d = Math.atan2(y1 - y0, x1 - x0);
d = d / Math.PI * 180;
return rotationCut((int) d);
}
分析:计算角度
// 通用读条开始通知
message SCCommonStartStripe{
required int32 stripeID = 1; //读条id,对应DictCommonStripe表中的id
repeated int32 params = 2; // 服务器透传参数
optional int32 objectId = 3; // 对象id
optional int32 rotation = 4; // 朝向 0-360
}
分析:最后在广播协议中加上朝向参数即可同步到周围玩家
登录
1.断线重连机制
当客户端直接断开时会走断线重连机制,如果是选择退出游戏那么不会走断线重连。
特别注意,这里的断线重连不单单指玩家直接杀进程,例如:手机直接关进程,unity直接重启游戏,
还包括手机切后台,如果玩家在打一个游戏途中,切出去和朋友进行微信聊天,时间长了,游戏并不是一直在后台进行游戏的,超过一定时间后,客户端会开始尝试重连。
一:服务端等待断线,如果玩家没有在规定的时间内重新登录,那么时间到了才会将对象从内存中移除。
大体流程如下:
1.客户端异常关闭,netty的chanel会断开,我们在exception里将当前状态置为“断线重连状态”
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception
{
CoreLog.CORE_NET.info("channel inactive, remote:{}", ctx.channel().remoteAddress());
exception(ctx, null);
}
private void exception(ChannelHandlerContext ctx, String reason)
{
if (entity == null)
{
return;
}
CoreLog.CORE_NET.info("channel exception, remote:{}, exception:{}",
ctx.channel().remoteAddress(), reason == null ? "" : reason);
InterfaceObject interfaceObject = entity.getInterfaceObject();
if (interfaceObject != null)
{
try
{
interfaceObject.setException(ObjectExceptionEnum.DISCONNECT);
}
catch (NullPointerException e)
{
CoreLog.CORE_NET.info("ClinetHanlder: interface object is null");
}
}
ctx.close();
entity = null;
}
2.场景tick对象时,发现对象状态为不正常状态,判断是断线重连后,设置断线重连时间
/**
* 场景对象的tick,包括玩家
* @param interval
*/
public void tickObject(int interval) 方法下
actor.closeChannel();
actor.clearSendPacket();
// 如果断线并且不是主动退出游戏,才走断线重连回调
// 主动断开,那么还没设置断线重连时间, 所以actor.isWaitingReconnect()返回false
if (!actor.isWaitingReconnect())
{
//断开连接的回调处理
actor.disConnected();
}
3.tick处理断线重连,如果超时那么就将发送消息到world执行玩家退出场景
/**
* tick处理断线重连
* @param interval
*/
public void tickReconnect(int interval)
{
if (!isWaitingReconnect())
{
return;
}
if (exitStatus == ActorExitGameStatusEnum.LEAVING_SCENE)
{
return;
}
if (exitStatus == ActorExitGameStatusEnum.EXIT)
{
exitStatus = ActorExitGameStatusEnum.LEAVING_SCENE;
// 发送退出场景
sendLeaveSceneMessage(true);
return;
}
waitReconnectRemainTime -= interval;
// 如果是逻辑错误了,不需要断线重连等待,直接踢掉
if (exceptionEnum == ObjectExceptionEnum.GAME_LOGIC_ERROR || exceptionEnum == ObjectExceptionEnum.LOGIC_KICKOFF)
{
waitReconnectRemainTime = 0;
}
if (waitReconnectRemainTime <= 0)
{
waitReconnectRemainTime = 0;
exceptionEnum = ObjectExceptionEnum.WAIT_RECONNECT_TIMEOUT;
BPLog.BP_SCENE.info("角色:{} 等待断线重连超时", actorID);
exitStatus = ActorExitGameStatusEnum.LEAVING_SCENE;
// 发送退出场景
sendLeaveSceneMessage(true);
return;
}
}
4.world上场景线里玩家的数据回收以及其它的一些world信息处理,将消息回传到场景将玩家状态置为保存数据状态
actor.setExitStatus(ActorExitGameStatusEnum.SAVE_DATA);
5.继续场景tick对象,判断玩家为保存数据状态,将玩家状态置为删除
if (actor.getExitStatus() == ActorExitGameStatusEnum.SAVE_DATA)
{
BPLog.BP_SCENE.info("scene:{}:{} actor:{} object:{} exception:{}",
sceneID, lineID, actor.getActorID(), actor.getObjectID(), exceptionEnum);
actor.deleteSafe();
continue;
}
if (object.isCanDestroy())
{
removeBpObject(object);
continue;
}
分析:进行移除对象
6.最后移除对象各种操作
doRemoveBpObjectFromScene()
分析:主要是把actor对象从场景里缓存的一些map里移除,如果场景人数为空,那么通知world回收该场景线。
二:在规定的时间内,玩家进行断线重连登录
大体流程: 1.客户端发起登录
/ 登录游戏请求
message CSPlayerLogin {
required string platformUid = 1; //玩家平台SDK的Uid
optional string appVersion = 2; //程序版本号
optional string resVersoin = 3; //资源版本号
optional string channelId = 4; //渠道id,每个包唯一
optional string deviceId = 5; //
optional DeviceInfo deviceInfo = 6; //
optional string authCode = 7; //登录验证码,由checkPlatformToken返回
optional int32 loginType = 8; //登录类型 0是玩家登录,1是本地开发测试登录(不验证token)
optional int32 serverId = 9; //服务器id
optional string sessionID = 10; // 如果是断线重连才传此值,不是的话不需要传,只要消息里有sessionID服务器就会按照断线重连处理
optional int32 globalId = 11; // 服务器唯一id
}
分析:断线重连,客户端会赋值sessionID,服务端根据该值判断是否断线重连
2.服务端接受到消息后,去世界上场景线缓存验证该玩家是否还在内存中
/**
* 玩家断线重连请求处理
* @param loginObject
* @param login
* @return
*/
public int playerReconnectRequest(BPLoginObject loginObject, BPLogic.CSPlayerLogin login)
分析:该方法里将登录状态置为重连状态,然后发送世界验证是否在场景线缓存
@MessageHandler(MessageIDConst.WS_ACTOR_RECONNECT_REQUEST)
public void handleActorReconnectRequest(AbstractBPScene service, BPActorReconnectReq message)
{
...
...
if (actor.isWaitingReconnect())
{
// 重置一下时间
actor.setWaitReconnectRemainTime();
service.sendServiceMessageToWorld(rsp);
}
}
分析:多次收到世界下发到场景里的断线重连请求回复方法,然后上面这个方法要特别注意下,这里会重置下断线重连的时间, 也就是如果玩家一直连接、断线、连接、断线,那么因为断线重连时间一直被重置,玩家数据就会一直在内存里。
3.收到world返回的断线重连请求
/**
* 收到world返回的断线重连请求
* @param result
* @param accountID
* @param actorID
* @param sceneID
* @param lineID
*/
public void actorReconnectResponse(int result, String accountID, long actorID, int sceneID, int lineID,
long createTime, int posX, int posZ)
分析:这里会将登录状态置为等待客户端进入场景,并发送客户端SCReconnectInfoNotice下发重连信息
4.客户端发起断线重连进入场景请求
// 断线重连进入场景请求
message CSReconnectEnterScene{
required int32 sceneID = 1; // 场景id
required int32 lineID = 2; // 线id
}
分析:服务端接受到消息后,将登录状态置为重连进入场景成功,然后发送世界处理
5.经过多次下发
@MessageHandler(MessageIDConst.WS_ACTOR_BIND_CHANNEL_REQUEST)
public void handleActorBindChannelRequest(AbstractBPScene service, BPActorBindChannelReq message)
{
...
...
Actor actor = service.getActorByActorID(actorID);
int result = actor.reconnectCallback(coreEntity);
}
分析:注意这里我们可以看到,玩家对象是根据玩家id值直接从内存获取到的。然后调用断线重连成功回调方法。
也就是说,断下重连的时间内,玩家的数据还是保存在服务器内存中没有删除。
断线重连,不会new一个新的Actor对象。
6.断线重连成功回调方法
/**
* 断线重连成功回调
* @param coreEntity
* @return
*/
public int reconnectCallback(AbstractCoreEntity coreEntity)
{
...
...
//发送断线重连的初始化数据
sendReconnectInitData();
}
/**
* 发送断线重连的初始化数据
*/
private void sendReconnectInitData()
{
sendReconnectNoticeToWorld();
sendInitData();
sendWorldData();
teamModule.sendToWorldToGetInit();
dropModule.sendReconnectData();
gameModule.sendReconnectData();
knightFeastModule.sendReconnectData();
//就绪
setRadioReady(true);
getScene().doInitScene(this,true);
}
/**
* 登录以后第一次推送一些用户的初始数据
* 本次登录只调用一次
*
* 同时断线重连也会调用这个sendInitData()方法,我们平常在该方法里添加的初始化数据,
* 第一次登录和断线重连都会进行调用。
*/
public void sendInitData()
{
}
分析:这里断线重连成功回调方法里调用初始化数据方法,就是我们平时各个模块对应的初始化数据。
2.玩家登录
1.客户端建立连接
客户端建立连接
/**
* 处理客户端通信
* Created by wangqiang on 2017/9/21.
*/
public class ClientHandler extends ChannelInboundHandlerAdapter
{
...
...
//这里spring获取bean每次都是新new一个bean出来
AbstractLoginObject object = (AbstractLoginObject) SpringContainer.getInstance().getBeanById("LoginObject");
object.setCoreEntity(entity);
entity.setInterfaceObject(object);
//建立新连接 加入到待进入的玩家队列
loginInterface.newConnected(object);
}
客户端建立的连接,加入队列
/**
* 登录service
* Created by wangqiang on 2017/9/21.
*/
public abstract class AbstractLoginService extends AbstractService implements InterfaceLogin
{
/**
* 待进入的玩家队列
*/
protected ConcurrentLinkedQueue<AbstractLoginObject> toEnterQueue = new ConcurrentLinkedQueue<>();
@Override
public void newConnected(AbstractLoginObject object)
{
toEnterQueue.add(object);
}
}
登录线程tick()处理
/**
* 登录服务
* Created by wangqiang on 2017/7/7.
*/
public abstract class AbstractBPLoginService extends AbstractLoginService
{
@Override
public void tick(int interval)
{
...
tickToEnter(interval);
tickQueueList(interval);
//处理已连接的输入
//解析客户端处理包,反射到对应的处理类
tickConnectedInput(interval);
...
}
}
1.登录请求
public void handleCSPlayerLogin(BPLogic.CSPlayerLogin packet, BPLoginObject loginObject)
{
...
...
int result = loginObject.playerLoginRequest(packet);
}
分析:发起登录请求,调用玩家登录请求方法
/**
* 玩家登录请求
*
* @param loginObject
* @param packet
* @return
*/
public int playerLoginRequest(BPLoginObject loginObject, BPLogic.CSPlayerLogin packet)
{
...
...
else
{
BPLog.BP_LOGIN.info("登录 账号:{} 发送主服验证, type:{} authCode:{}", accountID, loginType, authCode);
// 去主服验证token
checkFromMainServer(loginObject, loginType, authCode);
}
}
分析:去主服验证token
/**
* 向主服验证玩家登录token
* @param object
* @param loginType
* @param authCode
*/
public void checkFromMainServer(BPLoginObject object, int loginType, String authCode)
{
ReqCheckLoginToken req = new ReqCheckLoginToken();
req.setLoginType(loginType);
req.setAuthCode(authCode);
req.setPlafformUid(object.getAccountID());
req.setServerId(NetConfig.getInstance().getServerID());
req.setDeviceId(object.getDeviceID());
req.setClientIp(object.getRemoteAddr());
req.setAppVersion(object.getClientVersion());
req.setChannelId(object.getChannelID());
String data = JSON.toJSONString(req);
StringBuilder uriSb = new StringBuilder();
uriSb.append(NetConfig.getInstance().getMainServerUrl()).append(HttpApi.CHECK_LOGIN_TOKEN);
URI uri = URI.create(uriSb.toString());
//这里new一个去主服检查玩家登录token回调即AbstractHttpResponse(子类ToMainCheckLoginTokenCallback)
//发送到主服检验后,主服会会回应一个http响应
ToMainCheckLoginTokenCallback callback = new ToMainCheckLoginTokenCallback();
callback.setPlatformUID(object.getAccountID());
StringBuilder dataSb = new StringBuilder();
dataSb.append(HttpConstant.HTTP_CONTENT).append(HttpConstant.SYMBOL_EQUAL).append(StringUtils.urlEncode(data));
httpClientModule.asyncRequest(uri, HttpMethod.POST, dataSb.toString(), HttpConstant.HTTP_CONTENT_TYPE_FORM, callback);
}
分析:主要就是拼接参数,转json串,发起http请求到主服验证。
AbstractBPLoginService.java下:
/**
* 处理http
* login只会发送http请求,所以这里只处理的是http response
* @param interval
*/
private void tickHttp(int interval)
{
// 每个tick处理50条http返回
for (int i = 0; i < 50; i++)
{
BPHttpCallback callback = httpClientModule.pop();
if (callback == null)
{
break;
}
callback.callback();
}
}
进行tick回调处理
**
* 去主服检查玩家登录token回调
*
* @author guozhen
* @date 2018/3/14
*/
public class ToMainCheckLoginTokenCallback extends BPHttpCallback
{
@Override
public void callback()
{
...
...
loginService.loadLoginData(loginObject);
}
}
分析:主服验证成功,回调方法里,调用向db发送load登录数据请求
/**
* 向db发送load登录数据请求
* 如果没有登录数据,会直接创建一个出来(现在注释了)
* 是说,如果执行load数据库任务时发现没有数据,就创建一个新账号数据出来。
* @param loginObject
*/
public void loadLoginData(BPLoginObject loginObject)
{
...
...
BPLoadLoginDataReq req = new BPLoadLoginDataReq(MessageIDConst.LOAD_LOGIN_DATA_REQUEST);
//从角色池里拿出玩家数据对象,这里的角色中间过渡只是调用init将所有model模块集合加入到列表而已,没有数据
ActorDataCache dataCache = BPGlobals.getActorDataCache();
if (dataCache == null)
{
BPLog.BP_LOGIN.error("登录 actor cache池已满, account:{}", accountID);
loginObject.setException(ObjectExceptionEnum.INTERNAL_ERROR);
return;
}
....
loginObject.setFlowStatus(BPLoginFlowStatusEnum.LOAD_ACCOUNT_LOGIN_DATA);
}
分析:向db发送load登录数据请求,设置状态为正在load账号登录数据
@MessageHandler(MessageIDConst.LOAD_LOGIN_DATA_REQUEST)
public void handleLoadLoginDataRequest(BPDBService dbService, BPLoadLoginDataReq message)
{
ActorDBTask actorDBTask = new ActorDBTask();
actorDBTask.clear();
AbstractService sourceService = message.getLoginService();
if (sourceService == null)
{
return;
}
actorDBTask.setSourceService(sourceService);
actorDBTask.setOperateType(DBOperateTypeEnum.LOGIN);
actorDBTask.setActorDataCache(message.getActorDataCache());
dbService.addDBTask(actorDBTask);
}
分析:这里会new一个DB任务,然后调用addDBTask()添加到待处理的db任务列表…最后会交给线程池去执行DB任务,执行完DB任务会加到完成或失败列表, tick列表时load数据会返回给登录线程。
/**
* tick待处理的db task
* @param interval 上一个tick到本次tick的时间间隔
*/
private void tickHandleDBTask(int interval)
{
...
...
int removePosition = 0;
for (int i = 0; i < size; i++)
{
// 取出一个任务
AbstractDBTask dbTask = handleTaskList.get(removePosition);
if (dbTask == null)
{
break;
}
if (dbTask instanceof ActorDBTask)
{
long actorID = ((ActorDBTask) dbTask).getActorDataCache().getActorID();
//玩家是否在执行db任务
short isHandling = handlingTaskMap.get(actorID);
if (actorID < 0)
{
isHandling = WITHOUT_TASK;
}
// 没有在执行的同ID任务
if (isHandling == WITHOUT_TASK)
{
if (actorID > 0)
{
//有任务
handlingTaskMap.put(actorID, HAS_TASK);
}
handleTaskList.remove(removePosition);
}
else
{
//集合里已经有,说明重复执行
if (repeatHandleSet.contains(actorID))
{
// 遍历做一个合并
for (int j = 0; j < removePosition; j++)
{
AbstractDBTask abstractDBTask = handleTaskList.get(j);
if (abstractDBTask instanceof ActorDBTask)
{
long id = ((ActorDBTask) abstractDBTask).getActorDataCache().getActorID();
if (id == actorID)
{
// 一次只做一次合并 一次就break了
if (mergeDBTask(abstractDBTask))
{
handleTaskList.remove(j);
}
else
{
handleTaskList.remove(removePosition);
}
break;
}
}
}
}
else
{
removePosition++;
repeatHandleSet.add(actorID);
}
continue;
}
}
...
...
// 派发执行任务
dbHandleThreadPool.addThreadTask(dbTask);
}
repeatHandleSet.clear();
}
分析:这里主要就是从执行db任务列表取出db任务,然后派发执行。需要注意的是,这里会判断当前的actor是否正在执行db任务,如果正在执行,那么会做一次 合并db任务操作。
/**
* 合并db任务到待处理的DB任务列表
* @param dbTask
* @return boolean 是否合并了一个task,并且将缓存置空
*/
private boolean mergeDBTask(AbstractDBTask dbTask)
{
//得处理的db任务列表如果长度为0,那么不用合并
int size = handleTaskList.size();
if (size == 0)
{
//直接将当前db任务加入到待处理的db任务列表
addDBTask(dbTask);
return false;
}
boolean merge = false;
for (int i = 0; i < size; i++)
{
AbstractDBTask task = handleTaskList.get(i);
//判断两个任务是否为同一类任务
if (dbTask.isEqual(task))
{
//如果当前任务和遍历到待处理的db任务列表里不是同一个
if (task != dbTask)
{
task.mergeFrom(dbTask);
merge = true;
break;
}
}
}
if (!merge)
{
// 这里的修改原因是出现一种情况:handleTaskList.size!=0 && dbTask没有被合并,就直接走下面的释放任务了
addDBTask(dbTask);
return false;
}
// 释放公共数据内存池
if (dbTask instanceof ActorDBTask)
{
BPGlobals.releaseActorDataCache(((ActorDBTask)dbTask).getActorDataCache());
((ActorDBTask)dbTask).setActorDataCache(null);
}
else if (dbTask instanceof CommonDBTask)
{
BPGlobals.releaseCommonDataCache(((CommonDBTask)dbTask).getCommonDataCache());
((CommonDBTask)dbTask).setCommonDataCache(null);
}
return true;
}
分析:合并db任务
private void tickThreadPool(int interval)
{
...
...
else if (abstractDBTask.getOperateType() == DBOperateTypeEnum.LOAD)
{
// load数据需要返回给登录线程
if (abstractDBTask instanceof ActorDBTask)
{
ActorDBTask actorDBTask = ((ActorDBTask)abstractDBTask);
BPLoadActorDataRsp rsp = new BPLoadActorDataRsp(MessageIDConst.LOAD_ACTOR_DATA_RESPONSE);
rsp.setResult(result);
rsp.setActorDataCache(actorDBTask.getActorDataCache());
sendMessage(actorDBTask.getSourceService(), rsp);
continue;
}
else if(abstractDBTask instanceof CommonDBTask)
{
CommonDBTask commonDBTask = (CommonDBTask) abstractDBTask;
BPLoadCommonDataRsp rsp = new BPLoadCommonDataRsp(MessageIDConst.COMMON_DATA_LOAD_RESPONSE);
rsp.setCommonDataCache(commonDBTask.getCommonDataCache());
rsp.setResult(result);
sendMessage(commonDBTask.getSourceService(), rsp);
continue;
}
}
}
分析:tick结果,load数据返回给登录线程
/**
* 角色相关的db任务
* Created by wangqiang on 2017/8/29.
*/
public class ActorDBTask extends AbstractDBTask
{
@Override
public int execute()
{
else if (operateType == DBOperateTypeEnum.LOGIN)
{
result = actorDataCache.getAccountLoginModel().load();
// TODO 压测临时添加
BPLog.BP_DB.info("[DB执行线程统计], task:{} load login 时间:{} ms", this.getClass().getName(),
System.currentTimeMillis() - startTime);
}
}
}
当前状态为login状态,那么会调用load方法去取账号数据,如果没有账号数据就会新new一个账号数据出来。
(可能已经修改逻辑)
public void handleLoadActorDataResponse(AbstractBPLoginService loginService, BPLoadActorDataRsp message)
{
...
...
//非常注意这里是新new了一个actor对象
Actor actor = new Actor();
loginObject.setActor(actor);
actor.setCoreEntity(loginObject.getCoreEntity());
...
...
//挂在actor上的所有模块初始化
actor.init();
....
....
//挂在actor上的所有模块调用copyFrom方法从db赋值
boolean result = actor.copyFrom(actorDataCache);
if (!result)
{
loginObject.setException(ObjectExceptionEnum.INTERNAL_ERROR);
BPLog.BP_LOGIN.error("登录 账号:{} 角色:{} copy from数据失败", accountID, actorID);
// 释放data cache
BPGlobals.releaseActorDataCache(actorDataCache);
}
actor.initCommonData(loginObject);
//挂在actor上的所有模块afterLoad()方法处理
actor.afterLoad();
actor.setAllModuleModify(false);
//设置状态load角色数据成功
loginObject.setFlowStatus(BPLoginFlowStatusEnum.LOAD_ACTOR_DATA_SUCCESS);
loginObject.getLoginTimeOut().cancelTimeout();
loginObject.getLoginTimeOut().setTimeOutEnum(BPLoginTimeOutEnum.CLIENT_LOAD_SCENE);
// 释放data cache
BPGlobals.releaseActorDataCache(actorDataCache);
// 加载完角色数据后把玩家数据同步给主服
loginService.onActorLogin(loginObject);
...
...
}
分析:load角色数据回复流程:
1.特别注意,玩家下线登录是新new了一个Actor对象,放进内存。
2.挂在actor上的所有模块初始化
3.挂在actor上的所有模块调用copyFrom方法从db赋值
4.挂在actor上的所有模块afterLoad()方法处理
5.设置状态为登录成功
6.同步数据到主服
**
* 角色相关的db任务
* Created by wangqiang on 2017/8/29.
*/
public class ActorDBTask extends AbstractDBTask
{
@Override
public int execute()
{
...
...
if (operateType == DBOperateTypeEnum.LOAD)
{
for (int i = 0; i < size; i++)
{
AbstractActorDataModel actorDataModel = list.get(i);
result = actorDataModel.load();
if (result < 0)
{
BPLog.BP_DB.warn("db 角色:{} 模块:{} load 数据出错", actorDataModel.getActorID(), actorDataModel.getClass().getName();
break;
}
}
}
}
}
分析:DB任务最后会交给线程池去派发执行任务,然后调用ActorDBTask的execute()执行角色load任务,调用actorDataModel.load()将所有的各个模块数据加载到内存。
3.关闭服务器流程
一:基本流程
/**
* 关闭信号处理器
* 执行关闭流程:
* 1. 通知登录线程,不再处理新登录
* 2. 通知场景踢人,存玩家数据
* 3. 存储world公共数据
* 4. 调用exit,退出进程
* Created by wangqiang on 2018/1/16.
*/
public class SystemSignalHandler implements SignalHandler
{
@Override
public void handle(Signal signal)
{
BPLog.BP_SYSTEM.info("收到关闭服务器信号:{}, 开始走关闭服务器流程", signal);
shutdown();
}
public static void shutdown()
{
BPJVMShutdownMessage message = new BPJVMShutdownMessage(MessageIDConst.JVM_SHUTDOWN_NOTICE);
AbstractWorldService worldService = ServiceManager.getInstance().getWorldService();
worldService.sendMessage(worldService, message);
}
}
分析:java中提供了signal的机制(需要实现注册机制)。在sun.misc包下,属于非标准包。服务器关闭,handle接受到消息,调用shutdown()方法, 发送世界,走关闭服务器流程。
/**
* 1. 通知登录线程,不再处理新登录
* 2. 通知场景踢人,存玩家数据
* 3. 存储world公共数据
* 4. 调用exit,退出进程
* 关服
*/
public void shutdown()
{
// 通知登录线程,停止登录
{
BPLog.BP_SYSTEM.info("停服 通知登录线程");
BPJVMShutdownMessage message = new BPJVMShutdownMessage(MessageIDConst.WL_SHUTDOWN_SERVER_NOTICE);
sendMessage(ServiceManager.getInstance().getLoginService(), message);
}
BPLog.BP_SYSTEM.info("停服 关闭进入场景功能");
// 关闭场景进入功能
setShutdown(true);
// 通知场景线程玩家
{
BPLog.BP_SYSTEM.info("停服 广播给所有运行中的场景");
BPJVMShutdownMessage message = new BPJVMShutdownMessage(MessageIDConst.WS_SHUTDOWN_SERVICE_NOTICE);
broadcastMessageToRunningScene(message);
}
}
分析:世界线程接受到线程消息后,调用shutdown()处理
1. 通知登录线程,不再处理新登录
2. 通知场景踢人,存玩家数据(这里其实是保存到文件)
3. 存储world公共数据
4. 调用exit,退出进程
二:继续看通知登录线程jvm关闭
/**
* 停服,踢掉所有正在登录的客户端,同时不接收新登录的客户端
*/
public void shutdown()
{
isShutdown = true;
Iterator<BPLoginObject> iterator = connectedList.iterator();
while (iterator.hasNext())
{
BPLoginObject loginObject = iterator.next();
sendLoginError(loginObject, BPErrorCodeEnum.LOGIN_SERVER_SHUTDOWN);
iterator.remove();
}
Iterator<Map.Entry<String, BPLoginObject>> iterator1 = loginMap.entrySet().iterator();
while (iterator1.hasNext())
{
Map.Entry<String, BPLoginObject> entry = iterator1.next();
BPLoginObject loginObject = entry.getValue();
sendLoginError(loginObject, BPErrorCodeEnum.LOGIN_SERVER_SHUTDOWN);
iterator1.remove();
}
TLongObjectIterator<BPLoginObject> iterator2 = waitEnterSceneMap.iterator();
while (iterator2.hasNext())
{
iterator2.advance();
BPLoginObject loginObject = iterator2.value();
waitEnterSceneSet.remove(loginObject.getAccountID());
sendLoginError(loginObject, BPErrorCodeEnum.LOGIN_SERVER_SHUTDOWN);
iterator2.remove();
}
Iterator<Map.Entry<String, BPLoginObject>> iterator3 = reconnectMap.entrySet().iterator();
while (iterator3.hasNext())
{
BPLoginObject loginObject = iterator3.next().getValue();
sendLoginError(loginObject, BPErrorCodeEnum.LOGIN_SERVER_SHUTDOWN);
iterator3.remove();
}
BPLog.BP_LOGIN.info("停服 login收到停服请求 关闭登录");
BPLog.BP_LOGIN.info("[登录统计] 当前在线人数:{}, 排队人数:{}, 正在登录的人数:{}, 重连中的人数:{}",
loggedMap.size(), queueList.size(), loginMap.size() + connectedList.size(), reconnectMap.size());
}
分析:isShutdown状态置为true,然后是已连接的客户端队列、登录中的客户端map、等待进入场景的对象…
private void tickShutdown(int interval)
{
if (!isShutdown || !isStarted)
{
return;
}
shutdownRemain -= interval;
if (shutdownRemain <= 0)
{
// 停服超时,则继续倒计时
BPLog.BP_LOGIN.warn("停服超时");
shutdownRemain = 300000;
}
if (offlineSaveMap.isEmpty() && loggedMap.isEmpty())
{
isStarted = false;
BPLog.BP_LOGIN.info("登录线程已完成停服操作");
// 发送给world线程,通知world线程执行停服存储操作
BPJVMShutdownMessage message = new BPJVMShutdownMessage(MessageIDConst.LW_SHUTDOWN_SERVICE_DONE);
sendMessage(ServiceManager.getInstance().getWorldService(), message);
return;
}
}
分析:登录线程tick,发送world线程,通知world线程执行停服存储操作
/**
* 登录线程完成停服操作,也保存完了所有玩家的数据
*/
public void loginShutdownDone()
{
BPLog.BP_SYSTEM.info("world线程开始保存数据");
// 保存world上的数据
for (AbstractWorldModule module : saveModuleArray)
{
module.setModify(true);
}
tradingMarketModule.setShutdown(true);
serverInfoModule.setShutdownTimestamp((int) (nowTickTime / 1000));
CommonDataCache dataCache = BPGlobals.getCommonDataCache();
if (dataCache == null)
{
// 池中没有足够的对象了,要new一个
BPLog.BP_SYSTEM.error("common data cache pool is empty!");
dataCache = new CommonDataCache();
}
dataCache.copyFrom(this);
BPSaveCommonDataReq req = new BPSaveCommonDataReq(MessageIDConst.WD_SHUTDOWN_SAVE_REQ);
if (BPGlobals.getInstance().getServerMode() == ServerModeEnum.EMERGENCY_EXIT)
{
req.setSaveFile(true);
}
req.setDataCache(dataCache);
req.setSrcService(this);
sendMessage(ServiceManager.getInstance().getDbService(), req);
}
分析:登录线程完成停服操作,保存所有玩家的数据