Featured image of post Minecraft服务器

Minecraft服务器

开服经历回顾

📝 备注

所有Minecraft相关内容将转移至新站点,此处仅作存档,不再继续更新!

我从2025年2月初开始学习搭建MC服务器,到现在也学到了不少知识。以下是我的开服历程,若能对各位有所帮助,实在是我的荣幸。

服务端搭建

常见的服务端

目前MC服务端主要包括:原版、插件端、模组端和混合端四类。

类别 特点
原版 就和MC原版一样,没有特殊功能
插件端 能加插件,不能加模组
模组端 能加模组,不能加插件
混合端 既能加模组,也能加插件

似乎混合端功能更强大,但实际上它的兼容性更差,所以如果想开整合包服务器的话最好还是用模组端。

由于我目前没找到合适的模组代替插件,因此只能用混合端了。

以下是常见的服务端:

服务端 类别 支持
Spigot 插件端 全版本
Paper 插件端 全版本
Forge/Fabric/NeoForge 模组端 全版本
Catserver(猫端) 混合端 1.12.2/1.16.5/1.18.2
Mohist(墨端) 混合端 主要为1.20.1

安装 Java 环境

Java环境是MC服务器必备的运行环境。Java有多个版本,不同的MC服务端对应不同的Java。

MC版本 支持
1.12 - 1.16 Java 8
1.17 - 1.20 Java 17
1.21 以上 Java 21

Java可从Oracle官网下载(最新版需要注册账号才可下载)。如果不在意,也可从镜像站例如Adoptium下载。

使用.exe.msi文件安装的Java一般自动配置好了环境变量,如果使用.zip安装Java则需手动配置。

服务端启动流程

以Windows系统为例:

  1. 从网络上下载对应的服务端,通常是一个.jar文件。
  2. 新建一个文件夹,将服务端核心放在文件夹内。
  3. 在文件夹内新建一个文本文档,文档内输入java -jar 你的服务端核心名.jar,保存后将文件扩展名改为.bat
  4. 双击.bat文件,服务端将会运行并且在终端实时输出。这时通常会出现是否同意eula协议,此时须在文件夹内找到eula开头的文件,将其中的false改为true
  5. 再次启动.bat文件,服务端开启成功。

若为Linux系统,则需要将启动文件改为.sh文件,同时使用chmod +x xxx.sh赋予执行权限。

网络配置

搭建服务器最重要的是配置好网络,核心是确定服务器的IP地址和端口号。

局域网联机

局域网联机最简单,只需知道服务器运行的主机的IP地址即可。

Windows平台:WIN + R 打开cmd,输入ipconfig,查看Wifi(无线连接)或以太网(有线连接)下的IP地址。

Linux平台:命令行输入 ip a

通俗来说,通过同一个Wifi相连的设备都处于同一个局域网下。连接在同一个局域网下的设备,它们的设备IP地址仅最后一位不同。在同一个局域网下的设备可通过IP地址互相连接。

若路由器开启了AP隔离(大部分校园网等),则此方法可能失效。

内网穿透

请在网络上自行查找相关内容。

内网穿透是通过特定技术,将你的内网(如局域网)地址经服务器映射到公网上的过程。此时一个公网IP地址,如:frp-xxx.com:xxxxx就等同于你的局域网地址192.168.x.x

IPv6连接

这是目前成本最低,效果最好的连接方式。

首先必须确保你和你的伙伴都有IPv6访问,然后查看IP地址,这时应该可以看到一串240e:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx(必须为240e开头,这样才是公网IPv6),这时[IPV6地址]:端口号的形式就是服务器的IP地址。

目前IP技术分为IPv4和IPv6两种。IPv4地址为12个十进制数,每3个数字用.隔开;IPv6地址为28个16进制数,每4个数字用:隔开。由此可见,IPv6地址比IPv4多得多,所以每个IPv6地址都可以作为公网使用。

如果你拥有一个域名,则可以给该域名添加一个AAAA记录(如www.yourdomain.com)绑定到公网IPv6,再添加一个CNAME记录(如mc.yourdomain.com)指向该AAAA记录。这样就可以通过mc.yourdomain.com:port连接到服务器。

如果想去除端口号,则再添加一个SRV记录:

设置 参数
名称 _minecraft._tcp.mc
权重 5(满足取值要求即可)
端口 服务器的端口
目标 www.yourdomain.com

这样就可以通过mc.yourdomain.com直接连接。

服务器进阶

修改服务器配置

依据服务端核心的不同,修改方法也有所区别。

启动文件

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/bin/bash
set -e

# 1. 如果 MySQL 没装,就装(可选)
# sudo apt update && sudo apt install -y mysql-server

# 2. 确保 MySQL 服务已启动
sudo service mysql start
# sudo systemctl is-active --quiet mysql || { echo "MySQL 启动失败"; exit 1; }

# 3. 等待 3306 真正响应
until nc -z 127.0.0.1 3306; do
  echo "等待 MySQL 端口 3306 …"
  sleep 1
done

# 4. 启动 MC(stop正常退出,崩溃自动重启)
cd {"服务器所在目录"}
while true; do
    java -Xms4G -Xmx8G -jar mohist-1.20.1.jar
    EXIT_CODE=$?
    
    # 退出码 0 = 正常停止 (stop命令)
    if [ $EXIT_CODE -eq 0 ]; then
        echo "服务器正常关闭,退出脚本"
        break
    fi
    
    echo "服务器异常终止 (退出码: $EXIT_CODE),5秒后自动重启..."
    sleep 5
done

java -jar 你的服务端核心名.jar中,可在-jar之后加上-XmsAG -XmxBG表示给服务器至少分配A个G的内存,至多分配B个G的内存。

特别的,java可以替换为具体的Java/bin路径。若未指明,默认为环境变量中的Java。

server.properties

这是所有服务器都具备的配置文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
allow-flight=true                       #是否启用飞行
allow-nether=true                       #是否生成下界
broadcast-console-to-ops=true           #是否向管理员发送控制台消息
broadcast-rcon-to-ops=true
debug=false                             #是否开启调试模式
difficulty=normal                       #难度设置
enable-command-block=true               #是否启用命令方块
enable-jmx-monitoring=false
enable-query=false
enable-rcon=false
enable-status=true
enforce-secure-profile=true
enforce-whitelist=false
entity-broadcast-range-percentage=100
force-gamemode=false                    #是否强制设置玩家的游戏模式
function-permission-level=4             #命令等级设置,最高为4,最低为0 
gamemode=survival                       #游戏模式设置
generate-structures=true
generator-settings={}
hardcore=false
hide-online-players=false
initial-disabled-packs=
initial-enabled-packs=vanilla
level-name=play                         #游戏主世界存档名称
level-seed=                             #游戏主世界地图种子
level-type=minecraft\:normal            #主世界地图类型
max-chained-neighbor-updates=1000000
max-players=40                          #服务器允许的最多人数
max-tick-time=60000
max-world-size=29999984
motd=rd806's server                     #服务器说明,显示在多人游戏界面
network-compression-threshold=256
online-mode=false                       #是否启用在线模式
op-permission-level=4                   #管理员命令等级
player-idle-timeout=0
prevent-proxy-connections=false         #是否阻止代理连接
pvp=true                                #是否允许玩家互相攻击
query.port=25565    
rate-limit=0
rcon.password=
rcon.port=25575
require-resource-pack=false             #是否强制使用资源包
resource-pack=                          #资源包链接
resource-pack-prompt=
resource-pack-sha1=
server-ip=                              #服务器运行的IP地址,默认留空为127.0.0.1
server-port=30067                       #服务器运行的端口
simulation-distance=10                  #服务器模拟距离,建议设置为4~6
spawn-animals=true
spawn-monsters=true
spawn-npcs=true
spawn-protection=0                      #出生点保护范围
sync-chunk-writes=true
text-filtering-config=
use-native-transport=true
view-distance=10                        #服务器视野距离,建议设置为4~6
white-list=false                        #是否启用白名单

bukkit.yml

spiogt.yml

安装插件和模组

插件放至plugins文件夹

模组放至mods文件夹

服务器引入MySQL

对于多个服务器,如果想不同服务器之间共享玩家数据(如权限、经验等),可以使用MySQL数据库。

下面以服务端插件PowerRanks为例进行说明:

  1. 打开PowerRanks的配置文件,找到关于数据来源的配置。 将type改为mysql,并配置好端口、用户名和密码。这样在启动时,插件会自动从127.0.0.1:3306端口的root用户读入数据。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
...
storage:
  type: mysql
  mysql:
    host: 127.0.0.1
    port: 3306
    database: powerranks
    username: root
    password: *******
    ssl: false
    verbose: false
...
  1. 在MySQL的表中创建新表,命名为powerranks,此处名称应与配置文件中的database后的名称相同。
  2. 启动MC服务器,这样Powerranks插件就会自动在MySQL中创建数据表,可以对其进行相关操作。
📝 备注

使用MySQL的数据时,每次启动服务器前应当先启动MySQL客户端(sudo service mysql start),否则会报错。

使用Nginx反向代理

常见问题

服务端加载长时间停顿

服务端在加载阶段出现长时间停顿(即日志没有输出且未出现Preparing level "world")时,可能是出现网络访问超时。

例如,在落幕曲1.6.2服务端中,当服务端加载至

1
[xx:xx:xx]: Async Sound System initialized

可能会出现长达5分钟的停顿,且CPU占用为0。

此时新建一个终端,使用如下命令

1
2
3
4
5
# 找到 Java 进程 PID
ps aux | grep java

# 打印所有线程的堆栈信息
jstack <PID> > thread_dump.txt

打开生成的堆栈信息文件thread_dump.txt,找到35号线程的部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Full thread dump OpenJDK 64-Bit Server VM (17.0.18+8-Ubuntu-122.04.1 mixed mode, sharing):

...

"modloading-worker-0" #35 daemon prio=5 os_prio=0 cpu=7390.32ms elapsed=106.47s tid=0x000076b40eed00f0 nid=0x4e92 runnable  [0x000076b381dfc000]
   java.lang.Thread.State: RUNNABLE
	at sun.nio.ch.SocketDispatcher.read0(java.base@17.0.18/Native Method)
	- parking to wait for  <0x0000000617032388> (a java.util.concurrent.ForkJoinPool)
	at sun.nio.ch.SocketDispatcher.read(java.base@17.0.18/SocketDispatcher.java:47)
	at sun.nio.ch.NioSocketImpl.tryRead(java.base@17.0.18/NioSocketImpl.java:266)
	at sun.nio.ch.NioSocketImpl.implRead(java.base@17.0.18/NioSocketImpl.java:317)
	at sun.nio.ch.NioSocketImpl.read(java.base@17.0.18/NioSocketImpl.java:355)
	at sun.nio.ch.NioSocketImpl$1.read(java.base@17.0.18/NioSocketImpl.java:808)
	at java.net.Socket$SocketInputStream.read(java.base@17.0.18/Socket.java:966)
	at sun.security.ssl.SSLSocketInputRecord.read(java.base@17.0.18/SSLSocketInputRecord.java:484)
	at sun.security.ssl.SSLSocketInputRecord.readHeader(java.base@17.0.18/SSLSocketInputRecord.java:478)
	at sun.security.ssl.SSLSocketInputRecord.bytesInCompletePacket(java.base@17.0.18/SSLSocketInputRecord.java:70)
	at sun.security.ssl.SSLSocketImpl.readApplicationRecord(java.base@17.0.18/SSLSocketImpl.java:1465)
	at sun.security.ssl.SSLSocketImpl$AppInputStream.read(java.base@17.0.18/SSLSocketImpl.java:1069)
	at java.io.BufferedInputStream.fill(java.base@17.0.18/BufferedInputStream.java:244)
	at java.io.BufferedInputStream.read1(java.base@17.0.18/BufferedInputStream.java:284)
	at java.io.BufferedInputStream.read(java.base@17.0.18/BufferedInputStream.java:343)
	- locked <0x00000007134d3608> (a java.io.BufferedInputStream)
	at sun.net.www.http.HttpClient.parseHTTPHeader(java.base@17.0.18/HttpClient.java:826)
	at sun.net.www.http.HttpClient.parseHTTP(java.base@17.0.18/HttpClient.java:761)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(java.base@17.0.18/HttpURLConnection.java:1740)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(java.base@17.0.18/HttpURLConnection.java:1641)
	at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(java.base@17.0.18/HttpsURLConnectionImpl.java:224)
	at com.github.alexmodguy.alexscaves.server.misc.WebHelper.getURLContents(alexscaves@2.0.1/WebHelper.java:24)
	at com.github.alexmodguy.alexscaves.AlexsCaves.readModIncompatibilities(alexscaves@2.0.1/AlexsCaves.java:202)
	at com.github.alexmodguy.alexscaves.AlexsCaves.commonSetup(alexscaves@2.0.1/AlexsCaves.java:172)
	at com.github.alexmodguy.alexscaves.AlexsCaves$$Lambda$18719/0x000076b38ad5f788.accept(alexscaves@2.0.1/Unknown Source)
	at net.minecraftforge.eventbus.EventBus.doCastFilter(net.minecraftforge.eventbus/EventBus.java:263)
	at net.minecraftforge.eventbus.EventBus.lambda$addListener$11(net.minecraftforge.eventbus/EventBus.java:255)
	at net.minecraftforge.eventbus.EventBus$$Lambda$1730/0x000076b388779180.invoke(net.minecraftforge.eventbus/Unknown Source)
	at net.minecraftforge.eventbus.EventBus$$Lambda$4480/0x000076b388d164b8.invoke(net.minecraftforge.eventbus/Unknown Source)
	at net.minecraftforge.eventbus.EventBus.post(net.minecraftforge.eventbus/EventBus.java:319)
	at net.minecraftforge.eventbus.EventBus.post(net.minecraftforge.eventbus/EventBus.java:299)
	at net.minecraftforge.fml.javafmlmod.FMLModContainer.acceptEvent(javafmllanguage@1.20.1-47.4.0/FMLModContainer.java:162)
	at net.minecraftforge.fml.ModContainer.lambda$buildTransitionHandler$5(fmlcore@1.20.1-47.4.0/ModContainer.java:127)
	at net.minecraftforge.fml.ModContainer$$Lambda$4459/0x000076b388d55458.run(fmlcore@1.20.1-47.4.0/Unknown Source)
	at java.util.concurrent.CompletableFuture$AsyncRun.run(java.base@17.0.18/CompletableFuture.java:1804)
	at java.util.concurrent.CompletableFuture$AsyncRun.exec(java.base@17.0.18/CompletableFuture.java:1796)
	at java.util.concurrent.ForkJoinTask.doExec(java.base@17.0.18/ForkJoinTask.java:373)
	at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(java.base@17.0.18/ForkJoinPool.java:1182)
	at java.util.concurrent.ForkJoinPool.scan(java.base@17.0.18/ForkJoinPool.java:1655)
	at java.util.concurrent.ForkJoinPool.runWorker(java.base@17.0.18/ForkJoinPool.java:1622)
	at java.util.concurrent.ForkJoinWorkerThread.run(java.base@17.0.18/ForkJoinWorkerThread.java:165)

...

JNI global refs: 70, weak refs: 90

观察可知Alex’s Caves 模组在commonSetup阶段尝试从网络下载模组不兼容列表,但连接被阻塞了(SocketDispatcher.read0在等待网络响应):

1
2
3
at com.github.alexmodguy.alexscaves.server.misc.WebHelper.getURLContents(alexscaves@2.0.1/WebHelper.java:24)
at com.github.alexmodguy.alexscaves.AlexsCaves.readModIncompatibilities(alexscaves@2.0.1/AlexsCaves.java:202)
at com.github.alexmodguy.alexscaves.AlexsCaves.commonSetup(alexscaves@2.0.1/AlexsCaves.java:172)

继续在终端中执行ss -tnp | grep java获取网络状态,可得如下结果:

1
ESTAB 0  367    [2403:ac00:a201:10f4:a2dc:efb6:7d70:711c]:40888   [2606:50c0:8003::154]:443   users:(("java",pid=12938,fd=880))

这是一个 IPv6 连接,状态是ESTAB(已建立),但Send-Q367(第二列数字)。

正常情况下Send-Q应该是0367表示TCP发送缓冲区里有367字节数据,对端一直没有确认。表明你的服务器在等对方说"我收到了",但对方没回应。

目标地址2606:50c0:8003::154解析后是 GitHub 的 IPv6 地址(CDN 节点)。说明Alex’s Caves 正在从 GitHub 下载不兼容列表。但是有时 GitHub 的 IPv6 节点响应正常,有时这个节点丢包或延迟高,导致启动服务器时会概率地发生“卡住”的现象,而5分钟正好是HTTP响应超时的用时。

所以为什么日志中没有显示呢?我也不知道……

一劳永逸的解决方法是修改host文件,

1
sudo nano /etc/hosts

添加如下内容:

1
2
127.0.0.1 raw.githubusercontent.com
127.0.0.1 api.github.com

通过将 GitHub 的域名指向本地,模组的连接请求会立刻失败,从而瞬间跳过。

或者想办法与Github建立稳定连接,我们将其作为习题留给读者思考。

保存文件后,重新启动服务器即可。

网站总访客数:Loading

使用 Hugo 构建
主题 StackJimmy 设计