目录
网关介绍
mina服务端代码示例
消息编解码
私有协议栈定义
消息体序列化与反序列化
编码器设计
解码器设计
游戏服务器的网关,主要是用于管理手机客户端会话(包括建立与移除),负责接收来自手机客户端的请求协议,以及主动推送服务端的响应包。
在单一进程服务端架构里,网关跟游戏业务处理是在同一个进程里。为了提高通信吞吐量,一些服务端架构将网关作为一个独立进程。这种模式下,客户端请求全部由网关接收,再通过网关转发给服务端;另一方面,服务端下发的消息,也只能通过网关推送到客户端。由于只有客户端跟网关是一对一的socket连接,网关到服务端只需创建若干socket就可以完成全部通信任务,大大提高了服务端的负载能力。
本文讨论的为集成网关。
采用Java编写的服务器在选择通信框架技术上,要么选择Netty,要么选择Mina,很少有公司会去研发自己的通信框架。原因很简单,重新造轮子实现NIO服务器,开发成本非常高,需要自己去处理各种复杂的网络情况,诸如客户端重复接入,消息编解码,半包读写等情况。即使花费长时间编写出来的NIO框架投入到生产环境使用,等待框架稳定也要非常长的时间,而且一旦在生产环境出现问题,后果是非常严重的。
当然,Netty和Mina不仅仅用于游戏服务器开发。多个开源的跨进程通信(例如rpc)就使用它们作为网络通信基础。例如阿里鼎鼎大名的Dubbo框架。
个人感觉Mina更容易上手。这可能跟我先学Netty,对NIO框架有了一点皮毛认知有关(^_^)
本文选择介绍的通信框架为Mina。(github上的项目本身支持Mina/Netty自由切换)
一个简单的Mina服务端通信demo是非常简单的,主要代码无非就是以下几行:
1. 创建NioSocketAcceptor,用于监听客户端连接;
2. 指定通信编解码处理器;
3. 指定处理业务逻辑器,主要是接受消息之后的业务逻辑;
4. 指定监听端口,启动NioSocket服务;
主要代码如下:
public void start() throws Exception {
int serverPort = ServerConfig.getInstance().getServerPort();
IoBuffer.setUseDirectBuffer(false);
IoBuffer.setAllocator(new SimpleBufferAllocator());
acceptor = new NioSocketAcceptor(pool);
acceptor.setReuseAddress(true);
acceptor.getSessionConfig().setAll(getSessionConfig());
logger.info("mina socket server start at port:{},正在监听客户端的连接...", serverPort);
DefaultIoFilterChainBuilder filterChain = acceptor.getFilterChain();
filterChain.addLast("codec",
new ProtocolCodecFilter(SerializerHelper.getInstance().getCodecFactory()));
filterChain.addLast("moduleEntrance", new ModuleEntranceFilter());
filterChain.addLast("msgTrace", new MessageTraceFilter());
filterChain.addLast("flood", new FloodFilter());
//指定业务逻辑处理器
acceptor.setHandler(new ServerSocketIoHandler(new MessageDispatcher()));
//设置端口号
acceptor.setDefaultLocalAddress(new InetSocketAddress(serverPort));
//启动监听
acceptor.bind();
}
其中ServerSocketIoHandler继承IoHandlerAdapter,负责处理链路的建立,摧毁,以及消息的接收。当收到消息之后,先不进行业务处理,暂时打印消息的内容。
public class ServerSocketIoHandler extends IoHandlerAdapter {
@Override
public void sessionCreated(IoSession session) {
//显示客户端的ip和端口
System.out.println(session.getRemoteAddress().toString());
}
@Override
public void messageReceived(IoSession session, Object data ) throws Exception
{
Message message = (Message)data;
System.out.println("收到消息-->" + message);
}
}
网关主要处理客户端的链接建立,以及消息的接受与响应。而具体通信协议栈的设计,则涉及到数据编解码问题了。
下面主要介绍消息序列化与反序列化库的选择,以及介绍Mina处理粘包拆包的解决方案。
私有协议主要用于游戏项目内部客户端与服务端通信消息的格式定义。不同于http/tcp协议,私有协议只用于内部通信,所以不需要遵循公有协议标准。每个项目都使用自定义的通信协议,协议标准主要是开发方便,编解码速度快,通信字节量少等。
本文使用的消息定义如下:
/**
* 通信消息体定义
*/
public abstract class Message {
/**
* messageMeta, module of message
* @return
*/
public short getModule() {
MessageMeta annotation = getClass().getAnnotation(MessageMeta.class);
if (annotation != null) {
return annotation.module();
}
return 0;
}
/**
* messageMeta, subType of module
* @return
*/
public byte getCmd() {
MessageMeta annotation = getClass().getAnnotation(MessageMeta.class);
if (annotation != null) {
return annotation.cmd();
}
return 0;
}
public String key() {
return this.getModule() + "_" + this.getCmd();
}
}
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MessageMeta {
/** 消息所属模块号 */
short module();
/** 消息所属子类型 */
short cmd();
}
优点 | 缺点 | |
jdk | 官方血统 | 通信双方都是Java平台; 需要实现Serializable接口; 速度慢,体积大; |
json | 自说明,可读性高 | 数据量大,不利于传输 |
protobuf | 压缩率高 | 手动编写.proto文件; 数据肉眼无法识别 |
protostuff | 基于protobuff,不用写proto文件 |
JDK自带的序列化方式虽然简单,但速度慢,序列化后内容大,首先被排除。Goole的Protobuf自带光环,序列化速度快,数据小,理应被重用。但Protobuf有一个致命的缺点,就是需要手动编写.proto文件,这是一个扣分项。幸运的是,JProtobuf的出现,挽救了这种局面。通过JProtobuf注解,我们再也不用编写讨厌的.proto文件。
@MessageMeta(module=Modules.LOGIN, cmd=LoginDataPool.REQ_LOGIN)
public class ReqAccountLogin extends Message {
/** 账号流水号 */
@Protobuf(order = 1)
private long accountId;
@Protobuf(order = 2)
private String password;
public long getAccountId() {
return accountId;
}
public void setAccountId(long playerId) {
this.accountId = playerId;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "ReqLoginMessage [accountId=" + accountId + ", password="
+ password + "]";
}
}
public class JProtobufTest {
@Test
public void testRequest() {
ReqAccountLogin request = new ReqAccountLogin();
request.setAccountId(123456L);
request.setPassword("kingston");
Codec simpleTypeCodec = ProtobufProxy
.create(ReqAccountLogin.class);
try {
// 序列化
byte[] bb = simpleTypeCodec.encode(request);
// 反序列化
ReqAccountLogin request2 = simpleTypeCodec.decode(bb);
Assert.assertTrue(request2.getAccountId() == request.getAccountId());
Assert.assertTrue(request2.getPassword().equals(request.getPassword()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
编码器的完整代码如下:
public class MinaProtocolEncoder implements ProtocolEncoder {
@Override
public void dispose(IoSession arg0) throws Exception {
}
@Override
public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception {
IoBuffer buffer = writeMessage((Message) message);
out.write(buffer);
}
private IoBuffer writeMessage(Message message) {
// ----------------消息协议格式-------------------------
// packetLength | moduleId | cmd | body
// int short byte byte[]
IoBuffer buffer = IoBuffer.allocate(CodecContext.WRITE_CAPACITY);
buffer.setAutoExpand(true);
// 写入具体消息的内容
IMessageEncoder msgEncoder = SerializerHelper.getInstance().getEncoder();
byte[] body = msgEncoder.writeMessageBody(message);
// 消息元信息常量3表示消息body前面的两个字段,一个short表示module,一个byte表示cmd,
final int metaSize = 3;
// 消息内容长度
buffer.putInt(body.length + metaSize);
short moduleId = message.getModule();
byte cmd = message.getCmd();
// 写入module类型
buffer.putShort(moduleId);
// 写入cmd类型
buffer.put(cmd);
buffer.put(body);
// // 回到buff字节数组头部
buffer.flip();
return buffer;
}
}
public class MinaProtocolDecoder extends CumulativeProtocolDecoder {
private Logger logger = LoggerFactory.getLogger(MinaProtocolDecoder.class);
@Override
protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
if (in.remaining() < 4) {
return false;
}
IMessageDecoder msgDecoder = SerializerHelper.getInstance().getDecoder();
in.mark();
int length = in.getInt();
int maxReceiveBytes = 4096;
if (length > maxReceiveBytes) {
logger.error("单包长度[{}]过大,断开链接", length);
session.close(true);
return true;
}
if (in.remaining() < length) {
in.reset();
return false;
}
// 消息元信息常量3表示消息body前面的两个字段,一个short表示module,一个byte表示cmd,
final int metaSize = 3;
short moduleId = in.getShort();
byte cmd = in.get();
byte[] body = new byte[length - metaSize];
in.get(body);
Message msg = msgDecoder.readMessage(moduleId, cmd, body);
out.write(msg);
return true;
}
}