Netty+SpringBoot+protobuf3 搭建socket游戏网络层

网络包设计 (2+4+n)

[包头 2 Bytes] : 除了包头2个字节外包体的大小
[包体 4 Bytes] :包的协议ID
[包体 ] : protobuf 字节数据

小编使用的是gradle项目进行搭建游戏引擎,开发工具:eclipse
gradle项目引用的依赖:

allprojects {
    apply plugin: 'java'
    apply plugin: 'idea'
    apply plugin: 'eclipse'
    apply plugin: 'findbugs'
    compileJava.options.encoding = 'UTF-8'
    sourceCompatibility = 1.8
    targetCompatibility = 1.8
    ext.projectName = "$name"
    repositories {
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        jcenter()
    }
    dependencies {
    
        compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.5.1'
        compile group: 'io.netty', name: 'netty-all', version: '4.1.6.Final'
        compile group: 'org.springframework', name: 'spring-context', version: '5.0.7.RELEASE'
        compile group: 'org.springframework', name: 'spring-core', version: '5.0.7.RELEASE'
        compile group: 'org.springframework', name: 'spring-beans', version: '5.0.7.RELEASE'
        compile group: 'org.springframework', name: 'spring-context-support', version: '5.0.7.RELEASE'
        compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.4.15'        
        compile group: 'com.google.guava', name: 'guava', version: '25.1-jre'
        compile group: 'com.alibaba', name: 'fastjson', version: '1.2.58'    
        compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25'
        compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.7'      
        compile group: 'commons-lang', name: 'commons-lang', version: '2.6'     
        compile(
                fileTree(dir: '../libs', include: '*.jar'),
        )
        compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.0'
    }
    // findbugs
    tasks.withType(FindBugs) {
        ignoreFailures = true
        reports {
            xml.enabled = false
            html.enabled = true
        }
        reportLevel = "high"
    }
}
subprojects {
    sourceSets.main.java.srcDirs = ["src", "conf"]
    task initPath {
        sourceSets.main.java.srcDirs.each { it.mkdirs() }
    }
    //清除上次的编译过的文件
    task clearPj(type: Delete) {
        delete 'build', 'target'
    }
    task copyJar(type: Copy) {
        from configurations.runtime
        into('build/libs/lib')
    }
    //把JAR复制到目标目录
    task release(type: Copy, dependsOn: [build, copyJar]) {
        from 'conf'
        into('build/libs/conf') // 目标位置
    }
    task export(type: Copy) {
        from 'build/libs'
        into '../export/' + projectName
    }
    task zip(type: Zip) {
        from 'build/libs'
    }
    task copyzip(type: Copy) {
        from 'build/distributions'
        into '../export/zip/'
    }
}

main的启动方法
小编使用的是springboot的加载方式进行初始化相关的配置


/**
 * socket 启动类
 * 
 * @bk https://home.cnblogs.com/u/huanuan/
 * @简书 https://www.jianshu.com/u/d29cc7d7ca49
 * @Author 六月星辰
 * @Date 2020年1月10日
 */
@Slf4j
public class ScoketApp {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BeanConfig.class);
        context.start();
        SocketServer socket = context.getBean(SocketServer.class);
        new Thread(socket).start();
    }
}

/**
 * 服务配置
 * 
 * @bk https://home.cnblogs.com/u/huanuan/
 * @简书 https://www.jianshu.com/u/d29cc7d7ca49
 * @Author 六月星辰
 * @Date 2020年1月10日
 */
@Configuration
@PropertySource("classpath:/scoket.properties")
@ImportResource("classpath:/quartz.xml")
public class BeanConfig {
    @Autowired
    Environment env;
    @Autowired
    void init() {
        // 系统配置 设置
        String ip = env.getProperty("ip");
        int port = env.getProperty("port", int.class);
        System.err.println("ip = " + ip + "port = " + port);
    }

    @Bean
    SocketServer socketServer() {
        return new SocketServer(scoketInitializer(), env.getProperty("port", int.class));
    }
    @Bean
    SocketInitializer scoketInitializer() {
        return new SocketInitializer();
    }
}

scoket.properties的配置

#scoket的ip
ip=127.0.0.1
#socket的端口
port=8888

SocketInitializer 初始化器

/**
 * socket 初始器
 * 
 * @bk https://home.cnblogs.com/u/huanuan/
 * @简书 https://www.jianshu.com/u/d29cc7d7ca49
 * @Author 六月星辰
 * @Date 2020年1月11日
 */
@AllArgsConstructor
public class SocketInitializer extends ChannelInitializer < SocketChannel > {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        // TODO Auto-generated method stub
        ChannelPipeline pipeline = ch.pipeline();
        System.out.println("报告");
        System.out.println("信息:有一客户端链接到本服务端");
        System.out.println("IP:" + ch.localAddress().getHostName());
        System.out.println("Port:" + ch.localAddress().getPort());
        System.out.println("报告完毕");
        
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        pipeline.addLast("decoder",new Decoder());//解码器
        pipeline.addLast("encoder", new Encoder());//编码器
//        pipeline.addLast(new ByteArrayEncoder());
        pipeline.addLast(new ServerHandler()); // 客户端触发操作
    }

}

解码器 (TCP的包处理)

/**
 * 解码器
 * 
 * @bk https://home.cnblogs.com/u/huanuan/
 * @简书 https://www.jianshu.com/u/d29cc7d7ca49
 * @Author 六月星辰
 * @Date 2020年1月10日
 */
@Slf4j
public class Decoder extends MessageToMessageDecoder<ByteBuf> {

    /**
     * [包头 2 Bytes] : 除了包头2个字节外包体的大小</br>
     * [包体 4 Bytes] :包的协议ID </br>
     * [包体 ] : protobuf 字节数据</br>
     * 1.先读取2个字节并解析出剩下数据流的长度dataLength</br>
     * 2.如果剩下的数据流不满足dataLength的长度则继续等待</br>
     * 3.如果剩下的数据流满足dataLength的长度则放行
     */
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
        // out.add(Unpooled.wrappedBuffer(msg));//不进行任何的解析直接放行
        // 可读长度
        int readableBytes = msg.readableBytes();
        if (readableBytes < 4) {
            System.err.println("接受到信息! 可读长度小于4! 不进行解码!");
            return;
        }
        // 标记读取位置
        msg.markReaderIndex();
        int dataLength = msg.readShort();// 读取两个节 查看剩下的数据流长度
        if (msg.readableBytes() < dataLength) {
            log.info("剩下可读长度小于 dataLength! 剩下可读长度 = {} ", msg.readableBytes());
            // 移除读取标准位置
            msg.resetReaderIndex();
            return;
        }
        ByteBuf buf = Unpooled.buffer(dataLength + 2);
        buf.writeShort(dataLength);
        buf.writeBytes(msg);
        // 放行到hander层
        out.add(Unpooled.wrappedBuffer(buf));
    }
}

解码器

/**
 * 编码器
 * 
 * @bk https://home.cnblogs.com/u/huanuan/
 * @简书 https://www.jianshu.com/u/d29cc7d7ca49
 * @Author 六月星辰
 * @Date 2020年1月10日
 */
public class Encoder extends MessageToMessageEncoder<byte[]> {

    @Override
    protected void encode(ChannelHandlerContext ctx, byte[] msg, List<Object> out) throws Exception {
        // TODO Auto-generated method stub
          out.add(Unpooled.wrappedBuffer(msg));
    }
}

服务器Handler


/**
 * 服务器Handler
 * 
 * @bk https://home.cnblogs.com/u/huanuan/
 * @简书 https://www.jianshu.com/u/d29cc7d7ca49
 * @Author 六月星辰
 * @Date 2020年1月10日
 */
public class ServerHandler  extends SimpleChannelInboundHandler<ByteBuf> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        // TODO Auto-generated method stub
        short readShort = msg.readShort();//2个字节
        int cmd = msg.readInt();//4个字节
        int dataLeng= readShort -4;//protobuf的长度   
        byte[] result1 = new byte[dataLeng];
        msg.readBytes(result1);
        c2s_login_user parseFrom = GameProto.c2s_login_user.parseFrom(result1);
        System.err.println("收到客户端的protobuf信息 = "+parseFrom.toString()); 
    }
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // TODO Auto-generated method stub
        super.channelActive(ctx);
        System.err.println("客户端链接上!");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // TODO Auto-generated method stub
        super.exceptionCaught(ctx, cause);
    }
}

模拟客户端

/**
 * 模拟客户端发送
 * 
 * @bk https://home.cnblogs.com/u/huanuan/
 * @简书 https://www.jianshu.com/u/d29cc7d7ca49
 * @Author 六月星辰
 * @Date 2020年1月11日
 */
public class MyClientTest {
    public static void main(String[] args) {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class).handler(new MyClientInitializer());
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8888).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            eventLoopGroup.shutdownGracefully();
        }

    }
}

客户端初始化器

/**
 * 客户端初始化器
 * 
 * @bk https://home.cnblogs.com/u/huanuan/
 * @简书 https://www.jianshu.com/u/d29cc7d7ca49
 * @Author 六月星辰
 * @Date 2020年1月11日
 */
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        // TODO Auto-generated method stub
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        pipeline.addLast(new ByteArrayEncoder());
        pipeline.addLast(new ChunkedWriteHandler());
        pipeline.addLast(new MyClientHandler());
    }
}

客户端处理器

/**
 * 客户端处理器
 * 
 * @bk https://home.cnblogs.com/u/huanuan/
 * @简书 https://www.jianshu.com/u/d29cc7d7ca49
 * @Author 六月星辰
 * @Date 2020年1月11日
 */
public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //与服务端链接成功 并发送一个登录的请求
        //组建protobuf的字节数据
        Builder builder = c2s_login_user.newBuilder();
        builder.setAccount("123456789");
        builder.setPassword("123456789");
        byte[] byteArray = builder.build().toByteArray();
        
        //发送的长度 = 2 +4 +n
        int length = 6 + byteArray.length;
        int dataLength = 4 + byteArray.length;
        // 4个字节长度的命令号
        int cmd = 1001;// 命令号
        
        //获取发送长度的字节ByteBuf流
        ByteBuf buf = Unpooled.buffer(length);
        buf.writeShort(dataLength);//[包头 2 Bytes] : 除了包头2个字节外包体的大小
        buf.writeInt(cmd);//    [包体 4 Bytes] :包的协议ID
        buf.writeBytes(byteArray);//  [包体 ] : protobuf 字节数据
        
        byte[] newdata = new byte[buf.readableBytes()];
        buf.readBytes(newdata);
        
        //发送数据
        ctx.writeAndFlush(Unpooled.copiedBuffer(newdata)); // 必须有flush
    }

    /**
     * 处理服务端发送过来的消息
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        // TODO Auto-generated method stub
        System.out.println("读取服务端通道信息..");
    }

}

先启动服务端在启动客户端,效果如下

image.png

image.png

小编项目源码下载地址:
https://github.com/zhuhuanuan/nettySocket

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容