在这里我要吹一下netty了,如果你从0开始制作socket通讯,长连接,自定义协议,高并发,Netty就是绝配
如果你是小白,如果你对socket不太了解,想搭建一个稳定的java游戏服务器,你就选Netty
Netty的具体介绍和基础代码,请查找《Netty 权威指南》,如果你要用Netty,强烈建议你去读一下,你一定不会后悔
为什么用protobuf?数据压缩,跨平台,客户端h5,u3d,cocos 都能开发
下面进入正式的服务器开发,参考 Netty 权威指南之Google protobuf 编解码
新建服务器BaseServer.java
package com.xs.game.netty;
import org.apache.log4j.Logger;
import com.xs.game.protobuf.GameProtobuf;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
import io.netty.handler.logging.LoggingHandler;
public abstract class BaseServer {
protected static Logger log = Logger.getLogger(BaseServer.class);
public BaseServer() {
super();
}
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
private ServerBootstrap serverBootstrap;
public void start(){
this.bossGroup = new NioEventLoopGroup();
this.workerGroup = new NioEventLoopGroup();
try{
this.serverBootstrap = new ServerBootstrap();
this.serverBootstrap.group(bossGroup, workerGroup);
this.serverBootstrap.channel(NioServerSocketChannel.class);
//重复使用本地地址和端口
this.serverBootstrap.option(ChannelOption.SO_REUSEADDR, true);
//等待客户连接的超时时间
this.serverBootstrap.option(ChannelOption.SO_TIMEOUT, 10);
//启用Nagle算法
this.serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true);
//保持连接
this.serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
this.serverBootstrap.handler(new LoggingHandler());
this.serverBootstrap.childHandler(new ChildChannelHandler());
//绑定端口,同步等待成功
ChannelFuture f = this.serverBootstrap.bind(getPort()).sync();
log.info(this.getClass().getSimpleName()+" start success at "+getPort()+"..........");
//等待服务器监听端口关闭
f.channel().closeFuture().addListener( new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) throws Exception {
future.channel().close();
shutdown();
}
});
} catch (InterruptedException e) {
log.error(this.getClass().getSimpleName()+" start failed at "+getPort()+"..........", e);
}
}
protected class ChildChannelHandler extends ChannelInitializer{
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(new ProtobufVarint32FrameDecoder());
channel.pipeline().addLast(new ProtobufDecoder(GameProtobuf.ServerRequest.getDefaultInstance()));
channel.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
channel.pipeline().addLast(new ProtobufEncoder());
channel.pipeline().addLast(new ProtobufHander());
}
}
private void shutdown(){
this.bossGroup.shutdownGracefully();
this.workerGroup.shutdownGracefully();
}
public abstract int getPort();
}
把端口抽象出来,便于以后项目扩展
protobuf.proto
option java_package = "com.xs.game.protobuf";
option java_outer_classname = "GameProtobuf";
//客户端发送请求
message ServerRequest{
required int32 msgId = 1;
required bytes data = 2;
}
//服务器返回数据
message ServerResponse{
required int32 msgId = 1;
required bytes data = 2;
}
具体的实现在Netty权威指南中都有实现,不在细聊
重点聊一下msgId和data的作用
在游戏开发中,所有的客户端逻辑操作都对应了一个msgId,根绝msgId服务器找到相应的处理方法
msgId的作用
msgId在以往的开发中有两种形式
通过类名得到服务器的bean,再根据反射得到处理方法,此类型调试很方便,通过消息数据能快速找到消息处理单元,调试消息很方便,但是用到了反射,对于我这种性格的人是不能忍的
服务器做一个维护表记录着msgId和处理单元的关系
以上两种第二种更加符合我的开发习惯
data的作用
byte[] data的处理也有两种形式
byte[] -> string -> json
public void doPlayerLogin(JsonObject msg){
//做登录处理
}
我们可以在分发上层把byte[]转换成JsonObject
优点:此方式扩展性很强,发挥json的特性
缺点:消息数据体大,人数多后单服务器带宽有影响
byte[]->protobuf
此方式需要根据具体的处理逻辑找到相应的protobuf
举例如下,当服务器收到一条登录消息,根据MsgId找到登录处理单元
public void doPlayerLogin(byte[] data) throws Exception {
LoginMsg msg = LoginMsg.parseFrom(data);
//做登录处理
}
//不能做到
public void doPlayerLogin(LoginMsg msg){
//做登录处理
}
LoginMsg的转换只能放到处理单元里,因为我们不能在分发上层决定转换成哪个protobuf
优点:消息数据体小,转换性能好,框架结构确定死,参数名都有prtobuf文件确定好,前后端对接简单
缺点:随着项目消息增多,书写protobuf文件很头疼,服务器返回客户端的数据很复杂,要把所有返回数据都用protobuf书写出来,工作量还是很大。
自己写一个netty客户端,模拟发送一个ServerRequest
测试完成