从零开始搭建游戏服务器 第一节 创建一个简单的服务器架构

引言

由于现在java web太卷了,所以各位同行可以考虑换一个赛道,做游戏还是很开心的。

本篇教程给新人用于学习游戏服务器的基本知识,给新人们一些学习方向,有什么错误的地方欢迎各位同行进行讨论。

技术选型

本篇教程预计使用Java+Redis+Mongo

正文

本着先完成再完美的原则,从最简单的echo服务器开始。

在这里插入图片描述

Echo服务器就是,客户端发什么数据,服务端就原样返回回去。

创建基础架构

IDEA创建项目

在这里插入图片描述

我这边用Gradle进行依赖管理,使用的版本为 gradle8.0.2, openjdk19.

修改build.gradle导入几个基础开发包。

dependencies {
    //网络
    implementation group: 'io.netty', name: 'netty-all', version: '4.1.90.Final'
    //spring
    implementation group: 'org.springframework', name: 'spring-context', version: '6.0.6'
    //log
    implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.36'
    implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.2.11'
    implementation group: 'ch.qos.logback', name: 'logback-access', version: '1.2.11'
    implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.11'
    //lombok
    compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.24'
}

创建Bean配置类

@Configuration
@ComponentScan(basePackages = {"com.wfgame"})
public class GameBeanConfiguration {
}

创建主类

@Component
@Slf4j
public class GameMain {
    public static void main(String[] args) {
        // 初始化Spring
        AnnotationConfigApplicationContext springContext = new AnnotationConfigApplicationContext(GameBeanConfiguration.class);
        springContext.start();
        log.info("server start!");
    }
}

运行一下,正常输出server start!

我们会发现,程序执行后马上停止了,对于游戏服务器来说,我们需要保持运行状态,等待玩家接入进行游戏。所以我们main中增加一个循环,不停读取控制台输入,当读取到控制台输入stop时,我们再进行停服。

修改main方法如下:

    public static void main(String[] args) {
        // 初始化Spring
        AnnotationConfigApplicationContext springContext = new AnnotationConfigApplicationContext(GameBeanConfiguration.class);
        springContext.start();

        log.info("server start!");

        //region 处理控制台输入,每秒检查一遍 stopFlag,为true就跳出循环,执行关闭操作
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        // 设置循环使服务器不立刻停止
        while (true) {
            if (stopFlag) {
                log.info("receive stop flag, server will stop!");
                break;
            }
            // 每次循环停止一秒,避免循环频率过高
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //处理控制台指令
            try {
                if (br.ready()) {
                    String cmd = br.readLine().trim();
                    if (cmd.equals("stop")) {//正常关服
                        stopFlag = true;
                        log.info("Receive stop flag, time to stop.");
                    } else {
                        log.info("Unsupported cmd:{}", cmd);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        //停掉虚拟机
        System.exit(0);
    }

这样我们就获得了一个可以控制停服的服务器。当我们控制台输入stop时,程序结束运行。

添加Netty监听端口

要与客户端进行TCP连接,需要建立socket通道,然后通过socket通道进行数据交互。

传统BIO一个线程一个连接,有新的连接进来时就要创建一个线程,并持续读取数据流,当这个连接发送任何请求时,会对性能造成严重浪费。

NIO一个线程通过多路复用器可以监听多个连接,通过轮询判断连接是否有数据请求。

Netty对java原生NIO进行了封装,简化了代码,便于我们的使用。

Netty的包我们之前已经导入过了,直接拿来用即可。

首先我们创建一个Netty自定义消息处理类。

@Sharable
public class NettyMessageHandler extends SimpleChannelInboundHandler<Object> {
    /**
     * 读取数据
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        this.doRead(ctx, msg);
    }

    private void doRead(ChannelHandlerContext ctx, Object msg) {
        System.out.println("received msg = : " + msg);
        // 马上将原数据返回
        ctx.writeAndFlush(msg);
    }
}

然后编写Netty服务器启动代码,我们修改GameMain类的代码

@Component
@Slf4j
public class GameMain {

    // 停服标志
    private static boolean stopFlag = false;

    public static void main(String[] args) {
        // 初始化Spring
        AnnotationConfigApplicationContext springContext = new AnnotationConfigApplicationContext(GameBeanConfiguration.class);
        springContext.start();

        // 启动Netty服务器
        try {
            startNetty();
            log.info("Netty server start!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        log.info("server start!");

        //region 处理控制台输入,每秒检查一遍 stopFlag,为true就跳出循环,执行关闭操作
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        // 设置循环使服务器不立刻停止
        while (true) {
            if (stopFlag) {
                log.info("receive stop flag, server will stop!");
                break;
            }
            // 每次循环停止一秒,避免循环频率过高
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //处理控制台指令
            try {
                if (br.ready()) {
                    String cmd = br.readLine().trim();
                    if (cmd.equals("stop")) {//正常关服
                        stopFlag = true;
                        log.info("Receive stop flag, time to stop.");
                    } else {
                        log.info("Unsupported cmd:{}", cmd);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        //停掉虚拟机
        System.exit(0);
    }

    /**
     * 启动netty服务器
     */
    private static void startNetty() throws InterruptedException {
        int port = 2333;
        log.info("Netty4SocketServer start---Normal, port = " + port);

        final NioEventLoopGroup bossGroup = new NioEventLoopGroup(2);
        final NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workerGroup);
        bootstrap.channel(NioServerSocketChannel.class);
        bootstrap.option(ChannelOption.SO_REUSEADDR, true);//允许重用端口
        bootstrap.option(ChannelOption.SO_BACKLOG, 512);//允许多少个新请求进入等待
        bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);//是否使用内存池
        bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);    // 保持连接活动
        bootstrap.childOption(ChannelOption.TCP_NODELAY, false);    // 禁止Nagle算法等待更多数据合并发送,提高信息及时性
        bootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);//是否使用内存池

        final NettyMessageHandler handler = new NettyMessageHandler();
        bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ChannelPipeline cp = ch.pipeline();
                cp.addLast(new StringDecoder());
                cp.addLast(new StringEncoder());
                cp.addLast("handler", handler);
            }
        });
        // 绑定并监听端口
        bootstrap.bind(port).sync();//线程同步阻塞等待服务器绑定到指定端口

        // 优雅停机
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }));

        log.info("Netty4SocketServer ok,bind at :" + port);
    }

我们先创建了一个startNetty()方法,用于启动Netty服务器,同时绑定了端口2333

我们要注意一下initChannel这块代码,我们注册了String编码解码器,他们是用换行符作为一个消息的结束标志,因此我们等下通过客户端发送消息过来需要在行尾添加换行符。同时将我们自定义的消息处理类也注册进pipeline中,当客户端发送消息过来,先通过StringDecoder进行解码,然后流入自定义处理类中进行下一步处理。

至此服务端Netty接入完毕,我们下面编写一个客户端进行测试。

编写客户端进行测试

我们增加了ClientMain类,用socket与服务器进行连接,读取控制台输入上行到服务器,同时接受服务器下行的消息。

public class ClientMain {

    private static Socket socket = null;
    private static BufferedReader br = null;
    private static BufferedWriter writer = null;
    private static BufferedReader receivedBufferedReader = null;
    public static void main(String[] args) {
        // 新增连接到服务器
        startSocket();
    }

    /**
     * 启动socket连接
     */
    private static void startSocket() {
        try {
            socket = new Socket("127.0.0.1", 2333);
            br = new BufferedReader(new InputStreamReader(System.in));
            writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            receivedBufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            new Thread(() -> {
                try {
                    while (true) {
                        Thread.sleep(1000L);
                        String s = receivedBufferedReader.readLine();
                        if (s!=null && !s.equals("")) {
                            System.out.println("receive: " + s);
                        }
                    }
                } catch (IOException | InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
            while (true) {
                Thread.sleep(1000L);
                if (br.ready()) {
                    writer.write(br.readLine().trim() + "n");
                    writer.flush();
                }
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                if (receivedBufferedReader != null) {
                    receivedBufferedReader.close();
                }
                if (writer != null) {
                    writer.close();
                }
                if (br != null) {
                    br.close();
                }
                if (socket != null) {
                    socket.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

测试一下,我们先运行服务器,再运行客户端。

在客户端控制台下输入测试信息。

在这里插入图片描述

可以成功进行信息交互

总结

本节一共做了这么几件事:

  1. 项目的初步创建,通过build.gradle进行依赖包的管理。
  2. Netty服务器的启动,并且不断监听控制台输入,客户端上行数据的读取。
  3. 编写测试用客户端,与服务器进行数据交互。

下一节将进行注册登录的开发,内容将会比较多,感兴趣的点点关注或者留言评论。