版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/aiyoyoyo/article/details/79362919
本篇将通过注解和反射来介绍一种游戏服务器命令的接收和处理的方式,希望各位喜欢。
在 Netty实战手册(三)中,HandlerService有一段代码:
cmd.docommand( _ctx , ( ByteBuf ) _obj );
这里是接收消息的入口,通过它,我们需要来完成3件事:解析命令,找到实现类,通过反射执行方法。
HandlerService.java:
@Override
public void docommand( ChannelHandlerContext _ctx , ByteBuf _request ){
// 1. 约定4个字节的命令ID
int cmd = _request.readInt();
ByteBuf _response = _ctx.alloc().buffer();
try{
// 2. 通过注解找出命令对应的实现类和方法,保证效率的话,可以将命令和实现类存入静态的Map对象
// CommonContextHolder为自定义Spring的Bean对象管理类,参考jees-jsts
// 注解查找实现类
Collection< Object > action_collection = CommonContextHolder
.getApplicationContext().getBeansWithAnnotation( GameCommand.class ).values();
Iterator< Object > action_iterate = action_collection.iterator();
boolean dorequest = false;
while( action_iterate.hasNext() ){
Object a = action_iterate.next();
Method[] mths = a.getClass().getMethods();
// 注解查找实现方法
for ( Method m : mths ) {
GameRequest g = AnnotationUtils.findAnnotation( m , GameCommand.class );
if( g != null && cmd == g.value() ){
//3. 通过反射,调用命令处理的部分
dorequest = true;
ReflectionUtils.invokeMethod( m , a, _request, _response );
break;
}
}
if( dorequest ) break;
}
// 这里的处理仅仅是为了保证错误的数据不会被传递到客户端
_ctx.writeAndFlush( _response );
}catch (Exception e) {
// 出错后可以通知客户端,指定的错误信息
}
}
这种形式虽然不如直接switch-case写法效率高,但可维护性大大增强。通过自定义注解GameCommand,我们只需要关注功能逻辑即可。
GameCommand.java
@Target( {ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GameCommand {
public int value() default 0;
}
注解需要定义的地方不多,这里仅定义一个value用于写命令,并声明该注解可用于类和方法。
下面来看下如何写功能的实现:
GameActionDemo.java
/**
* 这里的@GameCommand注解用于让Spring容器可以获取到,并识别为指定的相关类
*/
@Component
@GameCommand
public class GameActionDemo {
@Autowired
GameCache game;
/**
* 命令实现
* @GameCommand 用于声明需要处理哪个命令,注意如果同一个命令被多个方法声明,按照前面的写法只会执行一个。
* @Transactional 用于声明数据事务的范围
* @param _request 为服务端接收的客户端数据
* @param _response 为服务端要发往客户端的数据
*/
@GameCommand( IGameRequest.CMD_ACTION_DEMO )
@Transactional
public void cmd_action( ByteBuf _request, ByteBuf _response ) {
// 接收数据示意
int int_val = _request.readInt();
long long_val = _request.readLong();
// 这里可以理解为功能处理,可以直接写这里,或者另外写个数据处理的类
game.dosomething( int_val, long_val );
// 写回数据示意
_response.writeInt( int_val );
_response.writeLong( long_val );
}
}
其实整个命令结构不会太复杂,复杂的是多样的功能需求和数据处理。另外需要额外说明和注意的地方有这么3点:
1. ChannelHandlerContext对象可以记录,这样我们可以有效的识别消息来源,用户数据等是否合法,并可以合法的传递回去。
2. 对于数据库处理,尽量集中完成,确保完成后在通知客户端。当然也可以使用缓存的方式处理,定时的更新缓存到数据库这种方式。
3. 重要事项:由于消息是异步传递的,在数据处理部分注意用户数据和数据库的原子性,特别是多用户数据交互的时候。存储对象尽量使用ConcurrentMap这类,适当的地方加上synchronized关键字。
Github: https://github.com/aiyoyoyo
讨论群:8802330