登录系统——分布式系统实现游戏不分区思路

登录系统——分布式系统实现游戏不分区思路

Scroll Down

前言

这些天在看项目的登录功能,思考游戏如何实现所有玩家同区,服务器满人不能和朋友一起玩,开新区会减少老区玩家会流失,还有一区情节等,也有人喜欢去新服新生态重新开始,但总体来说我认为弊大于利。

服务器方面

共用帐号服务器

有些每个服务器上都有web服,游戏服,每个区玩家绑定很难实现。
上家公司是一个web帐号服,一个世界服,对应一批游戏服,世界服是跨服活动方面,具体看:
跨服夺矿战实现
例如一个安卓帐号服+世界服,对应30个游戏服务器,ios帐号服对应10个游戏服,其它各渠道对应2个,现在想想应该是可以按安卓,ios,各渠道分区的。
帐号服务器主要用于玩家登录信息,还有支付订单,或者渠道特有的优惠礼包等活动奖励,公司项目其实已经完成。

游戏服务器分流

参考了这篇文章:类似于QQ游戏百万人同时在线的服务器架构实现
游戏分区主要原因还是服务器方面无法承受大量玩家同时在线,公司单个服务器一般5000用户左右。我的想法是,分区改成分频道或者分线路,有些游戏就是这种做法,但玩家登录web验证后,推荐人少的频道服务器,在游戏中每次切换频道实质就是切换其他游戏服务器,用户数据传过去不需要重新登录。维护也简单,关服直接关帐号服即可。

其它频道玩家交互

服务器器分流产生的问题是数据不一致,以前世界服是用来管理跨服活动,现在可以用来管理不同服务器玩家交流。
如果是公主连接那种没有其它用户界面,最多就添加好友,加工会的时候记录下id,查看成员的通过世界服查看成员id请求对方数据库服信息。如果是大型MMORPG,组队消息推送当前服务器,切换服务器也影响不大,队长进副本,世在界服创建新副本,拉取队伍成员用户。帮派帮战同理创建新地图即可,如果世界服压力也大,分类世界副本服,世界帮战服,王者荣耀不同区玩家跨服战斗原理应该差不多,世界服创建新战场。LOL,DOTA2也是,MOBA实现起来应该很简单,大量的战斗数据都在战斗服务器上。

安卓IOS用户同区

这个web帐号服放一起就行,加个字段区分安卓还是ios玩家。

数据库分流

服务器问题解决后来看看数据库,web服公用一个数据,储存帐号登录游戏验证密钥,支付订单等信息。
玩家没有固定服务器,可以把玩家以前绑定的服务器id变成数据库服务器id,实质绑定服务器变成了绑定数据库,登录时读取对应数据库信息,分流游戏服,世界服,数据库服都可以根据玩家数量变化而变化

登录系统

初始注册解析器,按id保存实体类,用户登录时根据平台id找到对应解析器验证

UserController用户接口

@Controller
@RequestMapping("/user")
public class UserController {
	private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);
	@Autowired
	private UserFacade userFacade;
	@Value("${server.secert}")
	private String serverSecert;
	@RequestMapping(value = "/authToken")
	@ResponseBody
	public TResult<AuthTokenResponse> authToken(@RequestBody JSONObject object, HttpServletRequest request) {
		LOGGER.debug("authToken request:{}", object.toJSONString());
		Integer platformId = object.getInteger("platformId");
		String channel = object.getString("channel");
		String token = object.getString("token");
		if (platformId == null || StringUtils.isBlank(channel) || StringUtils.isBlank(token)) {
			return TResult.valueOf(StatusCode.DATA_VALUE_ERROR);
		}
		TResult<User> result = userFacade.authToken(object);
		if (result.isFail()) {
			return TResult.valueOf(result.statusCode);
		}
		long uid = result.item.getUid();
		long loginTime = System.currentTimeMillis();
		token = SecurityUtils.hmacSHA1Encrypt(String.format(PlatformId.LOGIN_VALIDATE_KEY_FORMAT, platformId, uid, loginTime), serverSecert);
		AuthTokenResponse response = AuthTokenResponse.valueOf(uid, token, loginTime);
		return TResult.sucess(response);

	}
	/**
	 * 记录游戏登录
	 * 
	 * @param object
	 * @return
	 */
	@RequestMapping(value = "/recordLogin")
	@ResponseBody
	public Result recordLogin(@RequestBody JSONObject object, HttpServletRequest request) {
		long uid = object.getLongValue("uid");
		int serverId = object.getIntValue("serverId");
		if (uid == 0 || serverId == 0) {
			return Result.valueOf(StatusCode.DATA_VALUE_ERROR);
		}
		userFacade.updateLoginServer(uid, serverId);
		return Result.valueOf();
	}

UserFacadeImpl实现类

根据渠道id对应不用的渠道解析类

	@Override
	public TResult<User> authToken(JSONObject object) {
		Integer platformId = object.getInteger("platformId");
		String channel = object.getString("channel");
		PlatformInvoke parser = platformContext.getParser(platformId);
		if (parser == null) {
			LOGGER.error("PlatformInvoke not found, platformId:{}", platformId);
			return TResult.valueOf(StatusCode.DATA_VALUE_ERROR);
		}
		TResult<String> result = parser.login(object);
		if (result.isFail()) {
			return TResult.valueOf(result.statusCode);
		}
		if (platformId == PlatformId.QQ_GAME) {
			platformId = PlatformId.WAN_BA;
		}
		if (platformId == PlatformId.QQ_GAME_IOS) {
			platformId = PlatformId.WAN_BA_IOS;
		}
		User user = userDao.getUser(platformId, result.item);
		long now = System.currentTimeMillis();
		if (user.isDisabled() && now > user.getBeginTime() && now < user.getEndTime()) {
			return TResult.valueOf(ACTOR_HAS_DISABLED);
		}
		if (StringUtils.isBlank(user.getChannel())) {
			user.setChannel(channel);
			dbQueue.updateQueue(user);
		}
		return TResult.sucess(user);
	}

QQ_GAME渠道解析类

	@Override
	public TResult<String> login(JSONObject params) {
		try {
			String jsCode = params.getString("token");
			if (jsCode == null) {
				return TResult.fail();
			}
			Object pfObject = params.get("pf");
			String pf = "qqqgame";
			if (pfObject != null) {
				pf = pfObject.toString();
			}
			Object viaObject = params.get("via");
			String via = "qqqgame";
			if (viaObject != null) {
				via = viaObject.toString();
			}
			String mobile = params.getString("mobile");
			String mobiletype = params.getString("mobiletype");
			// Integer os = params.getInteger("os");
			Map<String, Object> checkParams = Maps.newHashMap();
			checkParams.put("appid", APPID);
			checkParams.put("secret", SECRET);
			checkParams.put("js_code", jsCode);
			checkParams.put("grant_type", "authorization_code");
			String response = HttpUtils.sendGet(CHECK_LOGIN_URL, checkParams);
			LOGGER.debug("QQGAME checkToken url:{},params:{},response:{}", CHECK_LOGIN_URL, checkParams, response);
			JSONObject jsonObject = JSONObject.parseObject(response);
			if (jsonObject.containsKey("openid")) {
				String openId = jsonObject.getString("openid");
				reportRegaccount(pf, openId, via, mobile, mobiletype, 1);
				Integer platformId = params.getInteger("platformId");
				if (platformId == PlatformId.QQ_GAME) {
					platformId = PlatformId.WAN_BA;
				}
				if (platformId == PlatformId.QQ_GAME_IOS) {
					platformId = PlatformId.WAN_BA_IOS;
				}
				User user = userDao.getUser(platformId, openId);
				user.setPf(pf);
				user.setVia(via);
				user.setLastLoginTime(System.currentTimeMillis());
				OPENID_SESSION_KEY_MAP.put(openId, jsonObject.getString("session_key"));
				return TResult.sucess(openId);
			} else {
				LOGGER.error("qqgame login url:{},request:{},response:{}", CHECK_LOGIN_URL, checkParams, response);
			}
		} catch (Exception e) {
			LOGGER.error("{}", e);
		}
		return TResult.fail();
	}

充值系统

登录流程基本就是这些,充值系统差不多更为简单

充值接口

充值商品id,创建订单等玩家支付

	@RequestMapping(value = "/inquiry", method = RequestMethod.POST)
	public @ResponseBody TResult<Map<String, Object>> inquiry(@RequestBody JSONObject object) {
		Result result = this.tokenValidate(object);
		if (result.isFail()) {
			return TResult.valueOf(result.statusCode);
		}
		if (JsonValidateUtils.paramValidate(object, "serverType", "serverId", "actorId", "shopItem", "ext") == false) {
			return TResult.valueOf(GameModuleStatusCodeConstant.INVALID_PARAM);
		}
		long userId = object.getLongValue("uid");
		int serverType = object.getIntValue("serverType");
		int serverId = object.getIntValue("serverId");
		long actorId = object.getLongValue("actorId");
		Map<String, Object> ext = object.getJSONObject("ext");
		ShopItem shopItem;
		try {
			shopItem = object.getObject("shopItem", ShopItem.class);
		} catch (Exception e) {
			return TResult.valueOf(GameModuleStatusCodeConstant.INVALID_PARAM);
		}
		TResult<Map<String, Object>> inquiryResult = buyOrderFacade.doInquiry(userId, serverType, serverId, actorId, shopItem, ext);
		return inquiryResult;
	}

支付回调接口

玩家支付成功后订单回调

@Controller
@RequestMapping(value = "/qqgame")
public class QQGameController {
	private static final Logger LOGGER = LoggerFactory.getLogger(QQGameController.class);
	@Autowired
	private QQGamePlatformImpl platformImpl;
	@Autowired
	private UserFacade userFacade;

	@Value("${server.secert}")
	private String serverSecert;

	@RequestMapping(value = "/deliver", method = RequestMethod.POST)
	public @ResponseBody TResult<Object> deliver(@RequestBody JSONObject params) throws IOException {
		LOGGER.error("deliver request  params:{}", params.toJSONString());
		try {
			return platformImpl.deliver(params);
		} catch (Exception e) {
			LOGGER.error(e.getMessage(), e);
		}
		return TResult.fail();
	}

支付校验

验证通过,订单状态改为成功发送奖励

	@Override
	public TResult<Object> deliver(JSONObject params) {
		try {
			String openid = params.getString("openid");
			if (openid == null) {
				return TResult.fail();
			}
			String selfSign = this.makePaySign(params);
			String originSign = (String) params.get(SIGN_KEY);
			if (originSign == null || !originSign.equalsIgnoreCase(selfSign)) {
				LOGGER.error("originSign:{},selfSign:{}", originSign, selfSign);
				return TResult.fail();
			}
			String ext = params.getString("app_remark");
			String[] strings = ext.split(",");
			if (strings.length != 6) {
				LOGGER.error("qqgame doDeliver ext error, ext :{}", ext);
			}
			// serverId,actorId,thirdId,orderId,chargeId,os
			int serverId = Integer.valueOf(strings[0]);
			long actorId = Long.valueOf(strings[1]);
			String thirdId = strings[2];
			String cpOrderId = strings[3];
			String chargeId = strings[4];
			String os = strings[5];
			LOGGER.debug(" qqgame serverId:{},actorId:{},thirdId:{},chargeId:{},cpOrderId:{},os:{}", serverId, actorId, thirdId, chargeId, cpOrderId,
					os);
			BuyOrder buyOrder = buyOrderFacade.getBuyOrder(actorId, cpOrderId);
			Double amount = params.getDouble("amt");
			if (buyOrder == null || !amount.equals(buyOrder.getAmount() * 10)) {
				return TResult.fail();
			}
			this.reportPay(thirdId, buyOrder.getAmount(), buyOrder.getUserId(), cpOrderId, os);
			buyOrder.setChargeId(Integer.parseInt(chargeId));
			buyOrder.setSuccessTimestamp(new Timestamp(System.currentTimeMillis()));
			dbQueue.updateQueue(buyOrder);
			buyOrderFacade.doDeliver(buyOrder);
			return TResult.sucess("success");
		} catch (Exception e) {
			LOGGER.error(e.getMessage(), e);
			e.printStackTrace();
		}
		return TResult.fail();
	}

多记录多思考蛮重要的,这些天写登录充值功能,想到服务器不分区问题,实现也有头绪了