按照以往运行 Minecraft 服务器的情况,玩家们常常需要一些游戏以外的信息:

  • 服务器负载情况
  • 服务器是否还活着
  • 是死宅要上传皮肤

那么,作为一个不怎么管服务器里发生了什么的管理,我就主动把这些功能实现了一下。其他的游戏内管理就交给 @kasora 了。

服务器负载

我们目前使用的是腾讯机房的双核 4G 的云服务器,讲道理这种阵容并不适合跑 MC 这种单线程大作。与此同时这次服务器应要求添加了村庄Mod(Millenaire),而 Millenaire 的作者自述此 Mod 和 Bukkit 存在兼容性问题,直接导致我们不再能使用 Bukkit 或者是基于 Bukkit 的服务端(例如 Cauldron),进而失去了 Bukkit API 和民间带来的一些优化。于是,在任意时刻知道服务器负荷以及一段时间内的负荷就显得比较有意义了。

至于如何去实现,就比较简单了。由于备案之类的种种问题,我们运行着 Minecraft Server 的服务器原则上不可以运行任何 HTTP Server 服务,所以我的思路是本机采集数据,然后通过某些办法发送到另一个已经备案了的服务器。

收集数据

在 Linux 设备上,我们有很多途径可以知道设备的负载和内存开销。系统负载的话,比如 `top`、`uptime` 命令,或者是 `/proc/loadavg` 这类文件,同样系统时间和内存占用也不难获得。于是我们可以准备这么一个 shell 脚本:


#!/bin/bash

TIMESTAMP=`date +%s`
LOAD=`cat /proc/loadavg | awk '{print $1}'`
MEM=`free -m | grep 'Mem' | awk '{print $3}'`

URL="https://status.mc.ntzyz.cn/set.php?timestamp=$TIMESTAMP&mem=$MEM&load=$LOAD"

curl $URL > /dev/null

执行一次,就能得到当前时间戳,1 分钟内负载和内存使用,并把这些数据作为请求发送到 status.mc.ntzyz.cn 所在的服务器了。这个脚本需要定时执行,使用 `crontab` 就可以完成。执行 `crontab -e`,并在文末添上:

* * * * * /bin/bash /home/ntzyz/send.sh

就可以实现每分钟一次请求了。

记录数据

服务器的各种数据发送到 status.mc.ntzyz.cn 之后,就需要将这些数据保存下来,准备随时被需要数据的情况下使用。是时候让全世界最好的语言上场了(雾


<?php
  if ($_SERVER["REMOTE_ADDR"] != "115.159.105.52") { // 防止某些人用自己的数据没事儿干瞎 GET
    $res->status = "ERROR";
    $res->message = "You shall not access.";
    die(json_encode($res));
  }

  $conn = mysql_connect("localhost", "_(:3」∠)_", "(╯-_-)╯╧╧");
  if (!$conn) {
    $res->status = "ERROR";
    $res->message = "Could not connect: " . mysql_error();
    die(json_encode($res));
  }

  $mem_used = $_GET["mem"];
  $cpu_load = $_GET["load"];
  $timestamp = time();
  if (!isset($mem_used) || !isset($cpu_load)) {
    $res->status = "ERROR";
    $res->message = "required field(s) is empty.";
    die(json_encode($res));
  }

  mysql_select_db("mc_stat", $conn);
  $sql = "insert into mc_stat(timestamp, cpu_load, mem_used) values('$timestamp', '$cpu_load', '$mem_used')";

  if (!mysql_query($sql,$conn)) {
    $res->status = "ERROR";
    $res->message = "SQL Error: " . mysql_error();
    die(json_encode($res));
  }
  else {
    $res->status = "success";
    echo(json_encode($res));
  }
?>

这样你就可以将使用 cURL 发来的数据保存到数据库了。然后就是再写一段脚本来读取这些数据,准备让其他人读取。


<?php
  header("Access-Control-Allow-Origin: *"); // Ajax is GOOD
  $conn = mysql_connect("localhost", "(╯-_-)╯╧╧", "_(:3」∠)_");
  $limit = 15;
  if (isset($_GET["limit"])) {
    $limit = $_GET["limit"];
  }

  if (!$conn) {
    $res->status = "ERROR";
    $res->message = "Could not connect: " . mysql_error();
    die(json_encode($res));
  }

  mysql_select_db("mc_stat", $conn);

  $res = mysql_query("select * from mc_stat order by timestamp desc limit $limit");
  $i = 0;

  $resp->status = "success";
  $resp->data = array();

  while ($row = mysql_fetch_array($res)) {
    $resp->data[$i]->timestamp = $row["timestamp"];
    $resp->data[$i]->cpu_load = $row["cpu_load"];
    $resp->data[$i]->mem_used = $row["mem_used"];
    $i = $i + 1;
  }

  die(json_encode($resp));
?>

数据的保存和读取就都完成了。

显示数据

世界上没什么比图形好的数据可视化方案了。我选择使用 highcharts 这个图表框架。由于只需要简单的显示两张折线图(负载和内存),我就直接对着 Demo 改了改,也算是 Make data come alive 了。

两张表的结构基本一致,所以只要准备一份模板,然后两次填入数据并使用框架就行了。首先是模板:


var template = {
  title: {
    text: 'Monthly Average Temperature',
    x: -20 //center
  },
  xAxis: {
    categories: null
  },
  yAxis: {
    plotLines: [{
      value: 0,
      width: 1,
      color: '#808080'
    }]
  },
  tooltip: {
    valueSuffix: ''
  },
  legend: {
    layout: 'vertical',
    align: 'right',
    verticalAlign: 'middle',
    borderWidth: 0
  },
  series: [{
    name: 'Tokyo',
    data: []
  }]
};

嗯,稍微了解过 Highcharts 的人都能看出,这基本就是第一个 Demo —— Basic Lines 的内容,只是少了一些个数据。然后就是使用 Ajax,将保存在 status.mc.ntzyz.cn 上的数据获得到浏览器,填入 template 并显示。


  $.ajax({
    url: "https://status.mc.ntzyz.cn/get.php?limit=" + limit,
    type: "GET",
    beforeSend: function() {
      $('#loadchart').html('获取数据中');
      $('#memchart').html('获取数据中');
    }
  }).done(function(data) {
    var res = JSON.parse(data);
    template.xAxis.categories = Array(limit).fill(0).map(function (it, off) {return limit - off;});
    template.title.text = 'Load Average';
    template.series[0].name = 'Load';
    template.series[0].data = Array(limit).fill(0).map(function (it, off) {return res.data[off].cpu_load * 1;}).reverse();
    $('#loadchart').highcharts(template);
    template.title.text = 'Memory Used';
    template.series[0].name = 'MB';
    template.series[0].data = Array(limit).fill(0).map(function (it, off) {return res.data[off].mem_used * 1;}).reverse();
    $('#memchart').highcharts(template);
  });

这样,就完成了所有的数据采集、记录和显示工作了。在那段 PHP 脚本中可以看出,只要修改 limit 的值,就能获得不同数量的数据,因此短时间的数据显示和长时间的数据显示就都可以完成了。

皮肤配置、上传与预览

去年暑假剁了正版 Minecraft 之后才知道这个游戏是有皮肤这个设定的,然而在 Online Mode 设置为 Off 的服务器上,客户端并不会去从 Mojang 的服务器上获得其他玩家的皮肤数据,这样别人就看不到我的皮肤了(雾

皮肤配置

由于没有使用正版验证的玩家是不能通过官方途径更换自己的皮肤,我们需要使用额外的 Mod 来实现皮肤的自定义。这里我们使用了 UniSkinMode配置的方法也不算麻烦。

皮肤上传

按照 UniSkinMod 的 API,我们需要准备每个玩家的 JSON 信息,同时准备一个 textures 目录来存储 PNG 格式的皮肤。上传皮肤只需要修改对应的 JSON 文件,同时添加/更新 textures 目录下对应的文件即可。综上,这些工作只需要一段 PHP 就可以完成。

<?php
  header("Access-Control-Allow-Origin: *");
  if (!isset($_GET['user']))
    die('something wrong');

  $user = $_GET['user'];
  $target_dir = "/var/www/skin/textures/";
  $target_file = $target_dir . $_GET['user'];
  $imageFileType = pathinfo($target_file,PATHINFO_EXTENSION);

  if (!move_uploaded_file($_FILES["file"]["tmp_name"], $target_file)) {
    if (!$_FILES["file"])
      die('file not set');
    die('上传失败');
  }
  else {
    $file = fopen("/var/www/skin/" . $user . ".json", "w");
    fwrite($file, "{"player_name": "". $user ."",  "last_update": 1416300800,  "model_preference": ["default"],  "skins": {    "default": "". $user .""  }}");
    fclose($file);
    die('上传成功');
  }
?>

皮肤预览

做这个纯粹是我比较想折腾折腾,毕竟 WebGL 出来这么多年了,同时也有 Three.js 这种很好用的 JavaScript 3D 库。这里的工作相当繁杂(然而逻辑很简单,都特么 UV 贴图数据要手撕)。想了解的可以直接去看下源码