Bootstrap

从零开始手写mmo游戏从框架到爆炸(一)— 开发环境

     

  一、创建项目

  1、首先创建一个maven项目,pom文件如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.loveprogrammer</groupId>
        <artifactId>eternity-online</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>eternity-client</artifactId>
    <packaging>jar</packaging>

    <name>eternity-client</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>

    </dependencies>

    <build>
        <plugins>
            <!-- https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-clean-plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-clean-plugin</artifactId>
                <version>3.1.0</version>
            </plugin>

            <!--
            编译插件
            mvn compile
            To compile your test sources, you'll do:
            mvn test-compile
            -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <compilerVersion>${java.version}</compilerVersion>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <!-- maven 3.6.2及之后加上编译参数,可以让我们在运行期获取方法参数名称。 -->
                    <parameters>true</parameters>
                    <skip>true</skip>
                </configuration>
            </plugin>

            <!-- 打包时跳过单元测试 https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-surefire-plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M5</version>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>

            <!-- 打包源码 https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-source-plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <version>3.2.1</version>
                <configuration>
                    <attach>true</attach>
                </configuration>
                <executions>
                    <execution>
                        <phase>compile</phase>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>
</project>

 2、先创建两个module,eternity-client、eternity-server,如下:

主pom文件增加:

  <modules>
    <module>eternity-client</module>
    <module>eternity-server</module>
  </modules>

二、Log4j2框架

       让我们先把日志弄好,目前原则上咱们整体不考虑使用spring框架,所以综合考虑我们就使用log4j2。

1、log4j2 的简介
        Apache Log4j2 是对Log4j 的升级版本,参考了logback 的一些优秀的设计,并且修复了一些问题,因此带来了一些重大的提升,主要有:

  1. 异常处理,在logback中,Appender中的异常不会被应用感知到,但是在log4j2中,提供了一些异常处理机制。
  2. 性能提升,log4j2 相较于log4j 和 logback 都具有明显的性能提升,有18倍性能提升,后面会有官方测试的数据。
  3. 自动重载配置,参考了logback的设计,当然会提供自动刷新参数配置,最实用的就是我们在生产上可以动态的修改日志的级别而不需要重启应用。
  4. 无垃圾机制,log4j2 在大部分情况下,都可以使用其设计的一套无垃圾机制【对象重用、内存缓冲】,避免频繁的日志收集导致的 jvm gc。

官网:https://logging.apache.org/log4j/2.x/

2、引入依赖

      在父pom文件中增加日志的依赖

         <!-- log4j2 日志门面 -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.11.1</version>
        </dependency>
        <!-- log4j2 日志实面 -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.11.1</version>
        </dependency>

         <!-- 使用slf4j 作为日志门面 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.26</version>
        </dependency>
        <!-- 使用 log4j2 的适配器进行绑定 -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.9.1</version>
        </dependency>

3、编写日志配置文件

 配置文件 eternity-server下 src/resources/log4j2.xml

<?xml version="1.0" encoding="UTF-8" ?>
<configuration status="warn" monitorInterval="5">
    <properties>
        <property name="LOG_HOME">logs/server</property>
    </properties>

    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n" />
        </Console>

        <File name="file" fileName="${LOG_HOME}/myfile.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />
        </File>

        <RandomAccessFile name="accessFile" fileName="${LOG_HOME}/myAcclog.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />
        </RandomAccessFile>

        <RollingFile name="rollingFile" fileName="${LOG_HOME}/myrollog.log"
                     filePattern="logs/server/$${date:yyyy-MM-dd}/myrollog-%d{yyyy-MM-dd-HH-mm}-%i.log">
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY" />
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %msg%n" />
            <Policies>
                <OnStartupTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="10 MB" />
                <TimeBasedTriggeringPolicy />
            </Policies>
            <DefaultRolloverStrategy max="30" />
        </RollingFile>

    </Appenders>

    <Loggers>
        <Root level="trace">
            <AppenderRef ref="Console" />
        </Root>
    </Loggers>
</configuration>

4、修改eternity-server 的启动main函数

public class EternityServerMain
{
    // 为了保证使用时,不需要每次都去创建logger 对象,我们声明静态常量
    public static final Logger LOGGER = LoggerFactory.getLogger(EternityServerMain.class);

    public static void main( String[] args )
    {
        LOGGER.info( "Hello World!" );
        LOGGER.info( "Hello END!" );
    }
}

三、基本netty网络编程

        我们来完成一个简单的netty网络编程

        首先完成服务端:eternity-server的开发,增加两个类:SocketServer.java、SocketServerHandler.java

 1、SocketServerHandler

package com.loveprogrammer.netty.simple;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @ClassName SocketServerHandler
 * @Description TODO
 * @Author admin
 * @Date 2024/1/29 17:29
 * @Version 1.0
 */
public class SocketServerHandler extends SimpleChannelInboundHandler<String> {
    private static final Logger logger = LoggerFactory.getLogger(SocketServer.class);
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        logger.info("数据内容:data=" + msg);
        String result = "我是服务器,我收到了你的信息:" + msg;
        result += "\r\n";
        ctx.writeAndFlush(result);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        logger.error("出现异常",cause);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        logger.info("建立连接");
        super.channelActive(ctx);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        logger.info("连接断开");
        super.channelInactive(ctx);
    }
}

  2、SocketServer

package com.loveprogrammer.netty.simple;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @ClassName SocketServer
 * @Description TODO
 * @Author admin
 * @Date 2024/1/29 17:08
 * @Version 1.0
 */
public class SocketServer {

    private static final Logger logger = LoggerFactory.getLogger(SocketServer.class);
    private static final String IP = "127.0.0.1";

    private static final int PORT = 8088;

    // 分配用于处理业务的线程组数量
    private static final int BIS_GROUP_SIZE = Runtime.getRuntime().availableProcessors() * 2;

    /**
     * 每个线程组中线程的数量
     */
    private static final int WORK_GROUP_SIZE = 4;

    private static EventLoopGroup bossGroup = new NioEventLoopGroup(BIS_GROUP_SIZE);
    private static EventLoopGroup workerGroup = new NioEventLoopGroup(WORK_GROUP_SIZE);

    public void run() throws Exception {
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup,workerGroup);
        bootstrap.channel(NioServerSocketChannel.class);
        bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ChannelPipeline pipeline = ch.pipeline();
                // 以(/n)为结尾分隔的 解码器
                pipeline.addLast("framer",new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
                pipeline.addLast("decoder",new StringDecoder());
                pipeline.addLast("encoder",new StringEncoder());
                pipeline.addLast(new SocketServerHandler());
            }
        });
        bootstrap.bind(IP,PORT).sync();
        logger.info("Socket服务器已经启动完成");
    }

    protected static void shutdown() {
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }

    public static void main(String[] args) throws Exception {
        logger.info("开始启动Socket服务器...");
        new SocketServer().run();
    }

}

       然后完成客户端:eternity-client的开发,增加两个类:SocketClient.java、SocketClientHandler.java

 1、SocketClientHandler 类

package com.loveprogrammer.netty.simple;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @ClassName SocketClientHandler
 * @Description TODO
 * @Author admin
 * @Date 2024/1/29 17:41
 * @Version 1.0
 */
public class SocketClientHandler extends SimpleChannelInboundHandler<String> {

    private static final Logger logger = LoggerFactory.getLogger(SocketClientHandler.class);
    @Override
    public void exceptionCaught(ChannelHandlerContext arg0, Throwable arg1) {
        logger.info("异常发生", arg1);
    }

    @Override
    public void channelRead(ChannelHandlerContext arg0, Object msg) throws Exception {
        super.channelRead(arg0, msg);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext arg0, String data) {
        logger.info("数据内容:data=" + data);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        logger.info("客户端连接建立");
        super.channelActive(ctx);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        logger.info("客户端连接断开");
        super.channelInactive(ctx);
    }

}

 2、SocketClient 类

package com.loveprogrammer.netty.simple;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @ClassName SocketClient
 * @Description TODO
 * @Author admin
 * @Date 2024/1/29 17:43
 * @Version 1.0
 */
public class SocketClient {

    private static final Logger logger = LoggerFactory.getLogger(SocketClient.class);
    private static final String IP = "127.0.0.1";
    private static final int PORT = 8088;

    private static EventLoopGroup group = new NioEventLoopGroup();

    protected static void run() throws InterruptedException {
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group);
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.handler(new ChannelInitializer() {
            protected void initChannel(Channel ch) throws Exception {
                ChannelPipeline pipeline = ch.pipeline();
                pipeline.addLast("framer",new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
                pipeline.addLast("decoder",new StringDecoder());
                pipeline.addLast("encoder",new StringEncoder());
                pipeline.addLast(new SocketClientHandler());
            }
        });

        // 连接服务器
        ChannelFuture channelFuture = bootstrap.connect(IP, PORT).sync();
        String msg = "大哥你好,我是客户端";
        msg += "\r\n";
        channelFuture.channel().writeAndFlush(msg);
        logger.info("向服务器发送消息 {}",msg);
        channelFuture.channel().closeFuture().sync();

    }

    public static void main(String[] args) throws InterruptedException {
        logger.info("开始连接Socket服务器...");
        try {
            run();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }
    }


}

下一章: 

从零开始手写mmo游戏从框架到爆炸(二)— 核心组件抽离与工厂模式创建-CSDN博客

全部源码详见:

gitee : eternity-online: 多人在线mmo游戏 - Gitee.com

分支:step-01

请各位帅哥靓女帮忙去gitee上点个星星,谢谢!

参考:

java游戏服务器开发: https://blog.csdn.net/cmqwan/category_7690685.html

log4j2 的使用【超详细图文】:log4j2 的使用【超详细图文】-CSDN博客

;