网关介绍
游戏服务器的网关,主要是用于手机客户端与游戏业务服务端通信的中转器,负责接收来自手机客户端的请求协议,以及推送服务端的响应包。
在单一进程服务端架构里,网关跟游戏业务处理是在同一个进程里。为了提高通信吞吐量,一些服务端架构将网关作为一个独立进程。这种模式下,客户端请求全部由网关接收,再通过网关转发给服务端;另一方面,服务端下发的消息,也只能通过网关推送到客户端。由于只有客户端跟网关是一对一的socket连接,网关到服务端只需创建若干socket就可以完成全部通信任务,大大提高了服务端的负载能力。
本文讨论的为集成网关。
采用Java编写的服务器在选择通信框架技术上,要么选择Netty,要么选择Mina,很少有公司会去研发自己的通信框架。原因很简单,重新造轮子实现NIO服务器,开发成本非常高,需要自己去处理各种复杂的网络情况,诸如客户端重复接入,消息编解码,半包读写等情况。即使花费长时间编写出来的NIO框架投入到生产环境使用,等待框架稳定也要非常长的时间,而且一旦在生产环境出现问题,后果是非常严重的。
Mina和Netty这两个框架的作者好像是同一个人。个人感觉Mina更容易上手。这可能跟我先学Netty,对NIO框架有了一点皮毛认知有关(^_^)
本文选择的通信框架为Mina。
mina服务端代码示例
一个简单的Mina服务端通信demo是非常简单的,主要代码无非就是以下几行:
1. 创建NioSocketAcceptor,用于监听客户端连接;
2. 指定通信编解码处理器;
3. 指定处理业务逻辑器,主要是接受消息之后的业务逻辑;
4. 指定监听端口,启动NioSocket服务;
主要代码如下:
public void start() throws Exception {
IoBuffer.setUseDirectBuffer(false);
IoBuffer.setAllocator(new SimpleBufferAllocator());
acceptor = new NioSocketAcceptor(pool);
acceptor.setReuseAddress(true);
acceptor.getSessionConfig().setAll(getSessionConfig());
//暂时写死在代码里,后期使用独立配置文件
int port = 9527;
logger.info("socket启动端口为{},正在监听客户端的连接", port);
DefaultIoFilterChainBuilder filterChain = acceptor.getFilterChain();
filterChain.addLast("codec", new ProtocolCodecFilter(MessageCodecFactory.getInstance()));
acceptor.setHandler( new IOHandler() );//指定业务逻辑处理器
acceptor.setDefaultLocalAddress(new InetSocketAddress(port) );//设置端口号
acceptor.bind();//启动监听
}
其中IoHandler继承自IoHandlerAdapter,负责处理链路的建立,摧毁,以及消息的接收。当收到消息之后,先不进行业务处理,暂时打印消息的内容。
package com.kingston.net;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
public class IoHandler 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协议,私有协议只用于内部通信,所以不需要遵循公有协议标准。每个项目都使用自定义的通信协议,协议标准主要是开发方便,编解码速度快,通信字节量少等。
本文使用的消息定义如下:
- 消息头
- 消息体
package com.kingston.net;
import com.kingston.net.annotation.Protocol;
/**
* 通信消息体定义
*/
public abstract class Message {
public short getModule() {
Protocol annotation = getClass().getAnnotation(Protocol.class);
if (annotation != null) {
return annotation.module();
}
return 0;
}
public short getCmd() {
Protocol annotation = getClass().getAnnotation(Protocol.class);
if (annotation != null) {
return annotation.cmd();
}
return 0;
}
public String key() {
return this.getModule() + "_" + this.getCmd();
}
}
/**
* 消息的元信息
* @author kingston
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MessageMeta {
/** 消息所属模块号 */
short module();
/** 消息所属子类型 */
short cmd();
}
编码器设计
JProtobuf的编解码非常简单,对于一个我们定义的请求消息,ReqLoginMessage类的playerId,password两个字段带有MessageMeta注解。
/**
* 请求-账号登录
* @author kingston
*/
@MessageMeta(module=Modules.LOGIN, cmd=LoginDataPool.REQ_LOGIN)
public class ReqLoginMessage 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 + "]";
}
}
package com.kingston.test.net;
import java.io.IOException;
import junit.framework.Assert;
import org.junit.Test;
import com.baidu.bjf.remoting.protobuf.Codec;
import com.baidu.bjf.remoting.protobuf.ProtobufProxy;
import com.kingston.game.login.message.ReqLoginMessage;
public class TestJProtobuf {
@Test
public void testRequest() {
ReqLoginMessage request = new ReqLoginMessage();
request.setPlayerId(123456L);
request.setPassword("kingston");
Codec<ReqLoginMessage> simpleTypeCodec = ProtobufProxy
.create(ReqLoginMessage.class);
try {
// 序列化
byte[] bb = simpleTypeCodec.encode(request);
// 反序列化
ReqLoginMessage request2 = simpleTypeCodec.decode(bb);
Assert.assertTrue(request2.getPlayerId() == request.getPlayerId());
Assert.assertTrue(request2.getPassword().equals(request.getPassword()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
编码器的完整代码如下:
public class ProtobufEncoder implements ProtocolEncoder {
private static Logger logger = LoggerFactory.getLogger(ProtobufEncoder.class);
@Override
public void dispose(IoSession arg0) throws Exception {
}
@Override
public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception {
CodecContext context = (CodecContext) session.getAttribute(SessionProperties.CODEC_CONTEXT);
if (context == null) {
context = new CodecContext();
session.setAttribute(SessionProperties.CODEC_CONTEXT, context);
}
IoBuffer buffer = writeMessage((Message) message);
out.write(buffer);
}
private IoBuffer writeMessage(Message message) {
//----------------消息协议格式-------------------------
// packetLength | moduleId | cmd | body
// int short short byte[]
IoBuffer buffer = IoBuffer.allocate(CodecContext.WRITE_CAPACITY);
buffer.setAutoExpand(true);
//消息内容长度,先占个坑
buffer.putInt(0);
short moduleId = message.getModule();
short cmd = message.getCmd();
//写入module类型
buffer.putShort(moduleId);
//写入cmd类型
buffer.putShort(cmd);
//写入具体消息的内容
byte[] body = wrapMessageBody(message);
buffer.put(body);
//回到buff字节数组头部
buffer.flip();
//消息元信息,两个short,共4个字节
final int METE_SIZE = 4;
//重新写入包体长度
buffer.putInt(buffer.limit() - METE_SIZE);
buffer.rewind();
return buffer;
}
private byte[] wrapMessageBody(Message message) {
//写入具体消息的内容
byte[] body = null;
Class<Message> msgClazz = (Class<Message>) message.getClass();
try {
Codec<Message> codec = ProtobufProxy.create(msgClazz);
body = codec.encode(message);
} catch (IOException e) {
logger.error("", e);
}
return body;
}
}
解码器设计
public class ProtobufDecoder implements ProtocolDecoder {
private static Logger logger = LoggerFactory.getLogger(ProtobufDecoder.class);
public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
//必须保证每一个数据包的字节缓存都和session绑定在一起,不然就读取不了上一次剩余的数据了
CodecContext context = SessionManager.INSTANCE.getSessionAttr(session, SessionProperties.CODEC_CONTEXT, CodecContext.class);
if (context == null) {
context = new CodecContext();
session.setAttribute(SessionProperties.CODEC_CONTEXT, context);
}
IoBuffer ioBuffer = context.getBuffer();
ioBuffer.put(in);
//在循环里迭代,以处理数据粘包
for (; ;) {
ioBuffer.flip();
//消息元信息常量4表示消息body前面的两个short字段,一个表示moduel,一个表示cmd,
final int METE_SIZE = 4;
if (ioBuffer.remaining() < METE_SIZE) {
ioBuffer.compact();
return;
}
//----------------消息协议格式-------------------------
// packetLength | moduleId | cmd | body
// int short short byte[]
int length = ioBuffer.getInt();
//int packLen = length + 4;
//大于消息body长度,说明至少有一条完整的message消息
if (ioBuffer.remaining() >= length) {
short moduleId = ioBuffer.getShort();
short cmd = ioBuffer.getShort();
byte[] body = new byte[length-METE_SIZE];
ioBuffer.get(body);
Message msg = readMessage(moduleId, cmd, body);
if (moduleId > 0) {
out.write(msg);
} else { //属于组合包
CombineMessage combineMessage = (CombineMessage)msg;
List<Packet> packets = combineMessage.getPackets();
for (Packet packet :packets) {
//依次拆包反序列化为具体的Message
out.write(Packet.asMessage(packet));
}
}
if (ioBuffer.remaining() == 0) {
ioBuffer.clear();
break;
}
ioBuffer.compact();
} else{
//数据包不完整,继续等待数据到达
ioBuffer.rewind();
ioBuffer.compact();
break;
}
}
}
private Message readMessage(short module, short cmd, byte[] body) {
Class<?> msgClazz = MessageFactory.INSTANCE.getMessage(module, cmd);
try {
Codec<?> codec = ProtobufProxy.create(msgClazz);
Message message = (Message) codec.decode(body);
return message;
} catch (IOException e) {
logger.error("读取消息出错,模块号{},类型{},异常{}", new Object[]{module, cmd ,e});
}
return null;
}
public void dispose(IoSession arg0) throws Exception {
}
public void finishDecode(IoSession arg0, ProtocolDecoderOutput arg1) throws Exception {
}
}