手游服务端框架之网关
来源:互联网 发布:js返回顶部 编辑:程序博客网 时间:2024/05/02 01:50
网关介绍
游戏服务器的网关,主要是用于手机客户端与游戏业务服务端通信的中转器,负责接收来自手机客户端的请求协议,以及推送服务端的响应包。
在单一进程服务端架构里,网关跟游戏业务处理是在同一个进程里。为了提高通信吞吐量,一些服务端架构将网关作为一个独立进程。这种模式下,客户端请求全部由网关接收,再通过网关转发给服务端;另一方面,服务端下发的消息,也只能通过网关推送到客户端。由于只有客户端跟网关是一对一的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;}@Overridepublic 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 {@Testpublic 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();}}}编码器的完整代码如下:
package com.kingston.net.codec;import java.io.IOException;import org.apache.mina.core.buffer.IoBuffer;import org.apache.mina.core.session.IoSession;import org.apache.mina.filter.codec.ProtocolEncoder;import org.apache.mina.filter.codec.ProtocolEncoderOutput;import com.baidu.bjf.remoting.protobuf.Codec;import com.baidu.bjf.remoting.protobuf.ProtobufProxy;import com.kingston.net.Message;import com.kingston.net.MessageFactory;import com.kingston.net.SessionProperties;public class MessageEncoder implements ProtocolEncoder{@Overridepublic void dispose(IoSession arg0) throws Exception {}@Overridepublic void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception {_encode(session, message, out);}public void _encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception {CodecContext context = (CodecContext) session.getAttribute(SessionProperties.CONTEXT_KEY);if (context == null) {context = new CodecContext();session.setAttribute(SessionProperties.CONTEXT_KEY, 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 = null;Class<Message> msgClazz = (Class<Message>) MessageFactory.INSTANCE.getMessage(moduleId, cmd);try {Codec<Message> codec = ProtobufProxy.create(msgClazz);body = codec.encode(message);} catch (IOException e) {e.printStackTrace();//logger}buffer.put(body);//回到buff字节数组头部buffer.flip();//重新写入包体长度buffer.putInt(buffer.limit()-4);buffer.rewind();return buffer;}}编码器代码比较简单,只要注意消息协议的格式,结合JProtobuf的编码即可。
解码器设计
package com.kingston.net.codec;import java.io.IOException;import org.apache.mina.core.buffer.IoBuffer;import org.apache.mina.core.session.IoSession;import org.apache.mina.filter.codec.ProtocolDecoder;import org.apache.mina.filter.codec.ProtocolDecoderOutput;import com.baidu.bjf.remoting.protobuf.Codec;import com.baidu.bjf.remoting.protobuf.ProtobufProxy;import com.kingston.game.login.message.ReqLoginMessage;import com.kingston.net.Message;import com.kingston.net.MessageFactory;import com.kingston.net.SessionProperties;public class MessageDecoder implements ProtocolDecoder{public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {_decode(session, in, out);}private void _decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) {//必须保证每一个数据包的字节缓存都和session绑定在一起,不然就读取不了上一次剩余的数据了CodecContext context = (CodecContext) session.getAttribute(SessionProperties.CONTEXT_KEY);if (context == null) {context = new CodecContext();session.setAttribute(SessionProperties.CONTEXT_KEY, context);}IoBuffer ioBuffer = context.getBuffer();ioBuffer.put(in);//在循环里迭代,以处理数据粘包for (; ;) {ioBuffer.flip();//常量4表示消息body前面的两个short字段,一个表示moduel,一个表示cmd,//一个short字段有两个字节,总共4个字节if (ioBuffer.remaining() < 4) {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-4];ioBuffer.get(body);Message msg = readMessage(moduleId, cmd, body);out.write(msg);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) {e.printStackTrace();}return null;}public void dispose(IoSession arg0) throws Exception {// TODO Auto-generated method stub}public void finishDecode(IoSession arg0, ProtocolDecoderOutput arg1) throws Exception {// TODO Auto-generated method stub}}
- 手游服务端框架之网关
- 手游服务端框架之模仿SpringMvc处理玩家请求
- 手游服务端框架之消息线程模型
- 手游服务端框架之配置与玩家数据库设计
- 手游服务端框架之使用Guava构建缓存系统
- 手游服务端框架之GM金手指的设计
- 手游服务端框架之客户端协议组合下发
- 手游服务端框架之每日重置逻辑
- 手游服务端框架之后台管理工具
- 手游服务端框架之使用事件驱动模型解决业务高耦合
- 手游服务端框架之使用Redis实现跨服排行榜
- 手游服务端开发
- Unity手游框架之 资源管理
- python手游服务端搭建
- TYPESDK手游聚合SDK服务端设计思路与架构之二:服务端设计
- 手游基本框架
- 端游、手游服务端常用的架构
- 手游服务端代码热部署
- js:制作一个简易的计数器:根据输入的两个整数和运算符,进行计算,然后输出计算结果
- 深入理解Javascript的继承和原型链
- 【脚本语言系列】关于PythonWeb服务器Nginx+uWSGI,你需要知道的事
- React Native 自定义控件之验证码和Toast
- 合并果子(优先队列)
- 手游服务端框架之网关
- Java EE的Struts 2使用笔记
- LeetCode OJ 1 Two Sum [Easy]
- nginx安装与http反向代理基本配置
- Spring框架中context-param与servlet中init-param的contextConfigLocation的区别
- sql 大全
- idea15入门
- 【linux 命令】文件相关命令
- Unity編輯器案列