Build a game server from scratch Section 1 Create a simple server architecture

introduction

Since the java web is too cumbersome now, everyone in the industry can consider changing the track, and it is still very fun to play games.

This tutorial is for newcomers to learn the basic knowledge of game servers, and to give newcomers some learning directions. If there are any mistakes, colleagues are welcome to discuss.

Technology Selection

This tutorial is expected to use Java+Redis+Mongo

text

In line with the principle of first completion and then perfection, start from the simplest echo server.

insert image description here

The Echo server is, what data is sent by the client, the server returns it as it is.

Create infrastructure

IDEA create project

insert image description here

I use Gradle for dependency management here, and the version used is gradle8.0.2, openjdk19.

Modify build.gradle to import several basic development packages.

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'
}

Create a Bean configuration class

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

create main class

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

Run it, and output server start normally!

We will find that the program stops immediately after execution. For the game server, we need to keep running and wait for players to connect to play the game. So we add a loop to the main to read the console input continuously, and when the console input stop is read, we stop the service again.

Modify the main method as follows:

    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);
    }

This way we get a server that can control downtime. When we enter stop in the console, the program ends.

Add Netty listening port

To establish a TCP connection with the client, it is necessary to establish a socket channel, and then perform data interaction through the socket channel.

Traditional BIO has one thread and one connection. When a new connection comes in, a thread is created and the data stream is continuously read. When this connection sends any request, it will cause a serious waste of performance.

A NIO thread can monitor multiple connections through the multiplexer, and judge whether the connection has a data request by polling.

Netty encapsulates java native NIO, which simplifies the code and facilitates our use.

We have imported the Netty package before, so we can use it directly.

First we create a Netty custom message processing class.

@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);
    }
}

Then write the Netty server startup code, we modify the code of the GameMain class

@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);
    }

We first created a startNetty() method to start the Netty server and bound port 2333

We should pay attention to the code of initChannel. We registered the String codec. They use a newline character as the end of a message, so we need to add a newline character at the end of the line when we send a message through the client. At the same time, our custom message processing class is also registered in the pipeline. When the client sends a message, it is first decoded by StringDecoder, and then flows into the custom processing class for the next step of processing.

At this point, the server-side Netty is connected, and we will write a client for testing.

Write a client for testing

We have added the ClientMain class to connect with the server through sockets, read the console input uplink to the server, and accept downlink messages from the server at the same time.

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();
            }
        }
    }
}

To test it out, let's run the server first, then the client.

Enter test information under the client console.

insert image description here

Information exchange can be successfully carried out

Summarize

This section has done a few things in total:

  1. The initial creation of the project uses build.gradle to manage dependent packages.
  2. The Netty server is started, and the console input is continuously monitored, and the client's upstream data is read.
  3. Write a test client to interact with the server.

The next section will be the development of registration and login. There will be more content. If you are interested, pay attention or leave a comment.

Guess you like

Origin blog.csdn.net/sinat_18538231/article/details/129740957