前言
服务端GitHub地址:https://github.com/griii/Hak-Server
小游戏网址:https://www.guorii.cn/hak
线程池构建
使用@Bean将构建出来的线程池放入Spring容器中
单个服务器的线程池,要求:核心100线程,无法添加线程,也就是说等待队列使用不存储的SynchronousQueue即可,拒绝策略为向用户返回拒绝值,或是向其他服务器进行负载均衡
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.*;
@Configuration
public class MainLogicThreadPool{
@Value("${MAX_THREAD_THRESHOLD}")
private int MAX_THREAD_THRESHOLD;
private static class RejectedHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
//先不做任何处理
}
}
@Bean
public ExecutorService mainLogicThreadPool(){
//线程池仅容载每个游戏房间的内容,不是容载每个用户的数量
//单个服务器的线程池,要求:核心100线程,无法添加线程,也就是说等待队列使用不存储的SynchronousQueue即可,拒绝策略为向用户返回拒绝值
return new ThreadPoolExecutor(MAX_THREAD_THRESHOLD,
MAX_THREAD_THRESHOLD,
60,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
Executors.defaultThreadFactory(),new RejectedHandler());
}
}
核心线程
核心线程仅需要通过MsgLogic不断地向所有Room内用户广播房间游戏信息,因此主要的内容就是游戏房间。
也就是说有两种方式:
1.MsgLogic内置一个Room作为变量
2.MsgLogic作为一个线程,使用ThreadLocal存储Room
似乎使用起来没有什么区别,所以还是用变量存吧
import com.guorui.hak.entity.room.AbstractRoom;
public class MainGameLogicThread implements Runnable {
//每个线程拥有一个Room
private AbstractRoom room;
@Override
public void run() {
}
}
后来又思考了一下,最好还是使用ThreadLocal吧,毕竟线程池核心线程如果设定不关闭,那么Room作为ThreadLocal也是可以重用的,否则每次都需要通过Spring新建一个原型显然不太好。
对于Room为后续扩展维护考虑,使用抽象类实现
//房间信息
@Scope("prototype")
@Component
public abstract class AbstractRoom implements IRoom {
protected final int INITIAL_CAPACITY = 6;
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : n + 1;
}
private ConcurrentHashMap<String, PlayerPeople> players = new ConcurrentHashMap(tableSizeFor(INITIAL_CAPACITY));
@Override
public ConcurrentHashMap<String, PlayerPeople> getUsers() {
return this.players;
}
}
设计Room接口的生命周期
由于依赖倒置原则,应当先设定好接口,再以此填充依赖…
Room即游戏房间,可以分为以下几个生命周期
1.加载(游戏使用匹配机制,加载就是匹配房间设定的人数后加载成功,这里可以使用CountDownLatch实现和开始周期的互动。)
2.开始(如上述,当游戏所有人数加载完毕,自动开始游戏内逻辑线程。)
游戏内逻辑仅由msgLogic组成,不断向所有用户广播游戏内信息,而用户的Instruct指令仅由Netty的单线程Handler负责。
3.结束(销毁所有相关线程,生成游戏记录)
以上接口方法由具体实现类实现
想了一下,加载过程不应由Room执行,应当配置匹配策略类,若游戏实行段位机制,维护一个同步队列
最终设计:
1.顶层Room接口
//通用房间接口
public interface IRoom {
//获取所有用户
ConcurrentHashMap<String,PlayerPeople> getUsers();
//获取Map容量,适用于初始化
int getCapacity();
//获取游戏线程是否进行中
boolean runNow();
//初始化
void init();
//加载
void load();
void start();
void finish();
}
二级抽象Room
//房间信息
public abstract class AbstractRoom implements IRoom {
//游戏房间是否正在执行,若正在执行则广播...
private volatile boolean isRun;
public boolean runNow(){
return isRun;
}
protected ConcurrentHashMap<String, PlayerPeople> players;
public void init(){
this.players = new ConcurrentHashMap<>(MapCapacityUtil.tableSizeFor(getCapacity()));
}
public AbstractRoom() {
init();
}
@Override
public ConcurrentHashMap<String, PlayerPeople> getUsers() {
return this.players;
}
}
初步实现的简单3V3房间
@Scope("prototype")
@Component
public class Default3V3Room extends AbstractRoom {
private static final int CAPACITY = 6;
@Override
public int getCapacity() {
return CAPACITY;
}
@Override
public void load() {
}
@Override
public void start() {
}
@Override
public void finish() {
}
}
核心线程版本1
@Scope("prototype")
@Component
public class MainGameLogicThread implements Runnable {
private IRoom room;
@Autowired
private ApplicationContext applicationContext;
@Autowired
RoomThreadLocal roomThreadLocal;
public MainGameLogicThread(ThreadLocal<IRoom> threadLocal,String roomType) {
if (threadLocal.get() == null){
threadLocal.set((IRoom) applicationContext.getBean(roomType));
}
}
public void reSetUsers(PlayerPeople[] playerPeople) throws RuntimeException{
if (!room.runNow()){
throw new RuntimeException("房间线程不在活动,无法修改!");
}
try{
Map<String,PlayerPeople> players = room.getUsers();
players.clear();//清空
for (PlayerPeople people:playerPeople){
players.put(people.getUid()+"",people);
}
}catch (Exception e){
throw new RuntimeException(e.getMessage());
}
}
@Override
public void run() throws RuntimeException {
Map<String,PlayerPeople> players = room.getUsers();
//如果游戏时间停止,线程终止...
while(room.runNow()){
//广播房间信息...
if (players == null || players.isEmpty()) {
throw new RuntimeException("游戏房间线程中没有玩家存在!");
}
for (Map.Entry<String, PlayerPeople> entry : players.entrySet()) {
JSONObject jsonObject = JSONObject.fromObject(players);
TextWebSocketFrameHandler.channelMap.get(entry.getKey() + "").writeAndFlush(new TextWebSocketFrame(jsonObject.toString()));
}
}
}
}
版本1有两个很致命的问题:
1.每个核心线程里都需要注入一个ApplicationContext用来创建Room,显然耦合非常严重
2.以我的思想,这个游戏应当是有匹配机制和创建房间两个机制。
对于匹配机制:
目前思路是创建一个RoomFactory工厂模式,维护一个优先队列用于匹配段位相近的对手
核心线程工厂
详情见注释
@Component
public class MatchRoomFactory implements IRoomFactory {
//优先队列映射集合
private Map<Class<? extends IRoom>, PriorityQueue<PlayerPeople>> queueMap = new ConcurrentHashMap<>();
@Autowired
private ApplicationContext applicationContext;
@Autowired
private ExecutorService mainLogicThreadPool;
//用户加入匹配...
public void join(String roomType,PlayerPeople playerPeople){
try {
Class<? extends IRoom> clazz = (Class<? extends IRoom>) Class.forName(roomType);
if (!queueMap.containsKey(clazz)) {
//如果不存在,要注意并发安全问题:不能创建多个相同的队列,也就是要做到单例模式
//由于这个并发问题出现的频率很小啊,所以直接锁就完了
synchronized (queueMap) {
if (queueMap.containsKey(clazz)) {
//经典双重检查
}
queueMap.put(clazz, new PriorityQueue<>());
}
}
Queue queue = queueMap.get(clazz);
queue.add(playerPeople);
try{
//CAPACITY作为实现类的final static字段,直接通过反射拿了
int capcity = (int) clazz.getDeclaredField("CAPACITY").get(clazz);
if (queue.size() >= capcity){
IRoom room = (IRoom) applicationContext.getBean(roomType);
PlayerPeople[] people = new PlayerPeople[capcity];
for (int i = 0; i < capcity; i++) {
people[i] = (PlayerPeople) queue.element();//用element方法会抛出异常
}
//调用room的生命周期:load周期,填充PlayerPeople
room.load(people);
//至此为止,匹配成功了,将这个room添加到核心线程的ThreadLocal中,线程添加到线程池,启动!
startThreadRoom(room);
}
}catch (Exception e){
if (e instanceof MyException){
//说明是内部再次抛出来的
throw e;
}
throw new RuntimeException("实现类的CAPACITY字段有误!");
}
}catch (Exception e){
throw new RuntimeException("加入匹配的房间类型格式错误!");
}
}
@Override
public void startThreadRoom(IRoom room) {
MainGameLogicThread mainGameLogicThread = (MainGameLogicThread) applicationContext.getBean("mainGameLogicThread");
mainGameLogicThread.setRoomLocal(room);//设置ThreadLocal
mainLogicThreadPool.submit(mainGameLogicThread);
}
}
修改Player实现Comparable
由于匹配机制中是通过优先队列匹配的,因此需要实现Comparable
后续应该会加入段位机制,通过段位来匹配,这里暂时只通过uid来匹配
@Data
public abstract class PlayerPeople implements IPlayer,Comparable<PlayerPeople> {
public int compareTo(PlayerPeople o){
return o.uid - this.uid;
}