Cómo crear una página de estado (online/offline) del servidor de MU
Aprende a construir una página de estado que muestra en tiempo real si el ConnectServer, el GameServer y la base de datos de tu MU Online están online, con conteo de jugadores e historial de uptime.
Una página de estado es uno de los elementos que más transmite confianza en un servidor de MU Online. El jugador quiere saber, antes de descargar el cliente, si el servidor está en línea, cuántas personas están jugando y si hubo caídas recientes. En este tutorial vas a montar una página completa en
Una página de estado es uno de los elementos que más transmite confianza en un servidor de MU Online. El jugador quiere saber, antes de descargar el cliente, si el servidor está en línea, cuántas personas están jugando y si hubo caídas recientes. En este tutorial vas a montar una página completa en PHP que prueba por TCP los puertos del ConnectServer y del GameServer, consulta la base para contar jugadores online y muestra todo en un panel con auto-refresh e historial de uptime. Todo el código está comentado y los valores concretos (puertos, nombres de tabla, campos) aparecen como ejemplo, ya que varían según la versión.
Requisitos previos
Antes de empezar, ten el entorno montado. Si todavía no tienes el servidor corriendo, mira primero cómo crear un servidor de MU Online y después vuelve a esta parte web.
| Requisito | Detalle (ejemplo — varía por versión) |
|---|---|
| Servidor web | Apache o Nginx con PHP 7.4+ (idealmente 8.1+) |
| Extensión de base | sqlsrv (SQL Server) o mysqli según tu distro |
| Acceso a los puertos | ConnectServer (ej.: 44405) y GameServer (ej.: 55901) alcanzables |
| Credenciales de la base | Usuario con permiso de lectura en la base de MU |
| Firewall | Puertos de chequeo liberados desde el host del sitio hacia el del juego |
El punto crítico es el acceso de red. Si el sitio está en el mismo VPS del servidor, pruebas 127.0.0.1. Si está en un host separado, necesitas liberar los puertos en el firewall del servidor de juego para el IP del sitio. Nunca dejes los puertos administrativos totalmente abiertos a internet solo por la página de estado — restríngelos por IP de origen.
Cómo funciona la detección de estado
Existen tres capas que vale la pena monitorear, y cada una responde a una pregunta diferente:
- ¿ConnectServer online? — es el servidor que el cliente contacta primero para listar los subservers. Si se cae, nadie entra. Lo probamos abriendo un socket TCP en su puerto.
- ¿GameServer online? — es donde el juego de hecho corre. Puede haber más de uno (subservers). Probamos el puerto de cada uno.
- ¿La base de datos responde? — si el SQL Server o el MySQL se cae, el login falla aunque los servidores estén en línea. Lo probamos con una query trivial.
La técnica principal es la prueba de puerto TCP: intentamos abrir una conexión. Si abre, el servicio está escuchando (online). Si la rechaza o expira, está offline. Esto no valida el protocolo de MU en sí, pero en la práctica es un indicador excelente y barato.
Paso 1 — Función que prueba un puerto TCP
Crea un archivo status/checker.php. La función central usa fsockopen con un timeout corto para nunca trabar la página:
<?php
// status/checker.php
/**
* Prueba si un puerto TCP está aceptando conexiones.
* Retorna true (online) o false (offline).
*/
function portaOnline(string $host, int $porta, float $timeout = 2.0): bool {
$errno = 0;
$errstr = '';
// @ suprime el warning cuando la conexión es rechazada — manejamos el retorno.
$conn = @fsockopen($host, $porta, $errno, $errstr, $timeout);
if ($conn === false) {
return false; // rechazada, filtrada o timeout = offline
}
fclose($conn);
return true;
}
/**
* Versión con medición de latencia (ms) — útil para mostrar "ping".
*/
function checarServico(string $host, int $porta, float $timeout = 2.0): array {
$inicio = microtime(true);
$online = portaOnline($host, $porta, $timeout);
$latencia = (int) round((microtime(true) - $inicio) * 1000);
return [
'online' => $online,
'latencia' => $online ? $latencia : null,
];
}
El parámetro timeout es lo que impide que la página se congele. Con 2 segundos, aunque los tres servicios estén offline, el peor caso es 6 segundos — y con caché eso se vuelve casi instantáneo.
Paso 2 — Definir los servicios monitoreados
Centraliza la configuración de los servicios en un array. Así, cuando agregues un subserver nuevo, editas un solo lugar:
<?php
// status/config.php
// ATENCIÓN: los puertos e IPs de abajo son EJEMPLO — varían por versión y por setup.
return [
'servicos' => [
'connect' => [
'nome' => 'Connect Server',
'host' => '127.0.0.1',
'porta' => 44405, // confírmalo en el ConnectServer.ini
],
'game_1' => [
'nome' => 'Game Server (Sub 1)',
'host' => '127.0.0.1',
'porta' => 55901, // confírmalo en el GameServer.ini
],
'game_2' => [
'nome' => 'Game Server (Sub 2)',
'host' => '127.0.0.1',
'porta' => 55902,
],
],
'cache_ttl' => 20, // segundos de caché del estado
];
Paso 3 — Contar jugadores online en la base
La prueba de puerto dice si el servicio vive, pero el jugador quiere ver cuántas personas están jugando. Eso viene de la base. En bases comunes de MU (Season 6 y derivadas), la tabla de personajes o de cuentas tiene un campo de estado de conexión. El nombre varía — puede ser ConnectStat, OnlineStatus o una tabla MEMB_STAT separada — así que ajústalo a tu schema.
<?php
// status/jogadores.php
require_once __DIR__ . '/db.php'; // tu función de conexión (getMuDB)
/**
* Cuenta los jugadores online leyendo el campo de estado de conexión.
* El nombre del campo/tabla VARÍA POR VERSIÓN — ajústalo a tu schema.
*/
function contarOnline(): ?int {
try {
$conn = getMuDB();
} catch (\Throwable $e) {
return null; // base offline
}
// Ejemplo SQL Server (Character.ConnectStat = 1 significa online)
$sql = "SELECT COUNT(*) AS total FROM Character WHERE ConnectStat = 1";
$stmt = sqlsrv_query($conn, $sql);
if ($stmt === false) {
return null;
}
$row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
return (int) ($row['total'] ?? 0);
}
En algunos setups, el conteo por ConnectStat puede quedar "atascado" tras una caída del GameServer (personajes marcados como online que en realidad se cayeron). Si eso pasa en tu servidor, prefiere leer de una tabla de estado actualizada por el propio GameServer, o pon el campo en cero al arrancar el servidor. Documenta qué método usaste.
Aquí está el SQL equivalente para quien usa MySQL/MariaDB, solo cambiando la sintaxis:
-- MySQL / MariaDB: contar personajes online
-- Campo de ejemplo — confirma el nombre en tu base
SELECT COUNT(*) AS total
FROM Character
WHERE ConnectStat = 1;
-- Alternativa: total de cuentas con sesión activa en una tabla de estado
SELECT COUNT(*) AS total
FROM MEMB_STAT
WHERE ConnectStat = 1;
Paso 4 — Armar el snapshot con caché
Chequear los puertos y la base en cada visita es un desperdicio y puede sobrecargar el GameServer si la página se vuelve viral. Guarda el resultado en un archivo de caché con un TTL corto:
<?php
// status/snapshot.php
require_once __DIR__ . '/checker.php';
require_once __DIR__ . '/jogadores.php';
function gerarSnapshot(): array {
$cfg = require __DIR__ . '/config.php';
$servicos = [];
$algumGameOnline = false;
foreach ($cfg['servicos'] as $chave => $svc) {
$res = checarServico($svc['host'], $svc['porta']);
$servicos[$chave] = [
'nome' => $svc['nome'],
'online' => $res['online'],
'latencia' => $res['latencia'],
];
if ($res['online'] && str_starts_with($chave, 'game')) {
$algumGameOnline = true;
}
}
return [
'atualizado' => date('c'),
'servicos' => $servicos,
// Solo cuenta jugadores si al menos un GameServer respondió.
'online' => $algumGameOnline ? (contarOnline() ?? 0) : 0,
'db_ok' => contarOnline() !== null,
];
}
/**
* Retorna el snapshot del caché, o genera uno nuevo si expiró.
*/
function getStatus(): array {
$cfg = require __DIR__ . '/config.php';
$cacheFile = sys_get_temp_dir() . '/mu_status.json';
$ttl = $cfg['cache_ttl'];
if (is_file($cacheFile) && (time() - filemtime($cacheFile)) < $ttl) {
$dados = json_decode(file_get_contents($cacheFile), true);
if (is_array($dados)) {
return $dados;
}
}
$snapshot = gerarSnapshot();
file_put_contents($cacheFile, json_encode($snapshot));
return $snapshot;
}
Paso 5 — Endpoint JSON para el front-end
Para que el panel se actualice sin recargar la página entera, expón el estado como JSON:
<?php
// status/api.php
require_once __DIR__ . '/snapshot.php';
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode(getStatus(), JSON_UNESCAPED_UNICODE);
Paso 6 — Página HTML con auto-refresh
Ahora la interfaz. Este fragmento consume el api.php y actualiza los indicadores cada 30 segundos, sin recargar la página:
<!-- status/index.html -->
<div id="status-painel" class="status-painel">
<div class="status-header">
<span id="status-geral" class="badge">Verificando...</span>
<span id="status-online">— jugadores online</span>
</div>
<ul id="status-lista"></ul>
<small id="status-hora"></small>
</div>
<script>
async function atualizarStatus() {
try {
const r = await fetch('/status/api.php', { cache: 'no-store' });
const d = await r.json();
// Indicador general: online si cualquier game server responde
const algumOnline = Object.values(d.servicos).some(s => s.online);
const geral = document.getElementById('status-geral');
geral.textContent = algumOnline ? 'ONLINE' : 'OFFLINE';
geral.className = 'badge ' + (algumOnline ? 'on' : 'off');
document.getElementById('status-online').textContent =
d.online + ' jugadores online';
// Lista de servicios
const lista = document.getElementById('status-lista');
lista.innerHTML = '';
for (const chave in d.servicos) {
const s = d.servicos[chave];
const li = document.createElement('li');
const ping = s.online ? ` (${s.latencia} ms)` : '';
li.innerHTML = `<span class="dot ${s.online ? 'on' : 'off'}"></span>
${s.nome}: <strong>${s.online ? 'Online' : 'Offline'}</strong>${ping}`;
lista.appendChild(li);
}
document.getElementById('status-hora').textContent =
'Actualizado: ' + new Date(d.atualizado).toLocaleTimeString('es-419');
} catch (e) {
document.getElementById('status-geral').textContent = 'ERROR';
}
}
atualizarStatus();
setInterval(atualizarStatus, 30000); // cada 30s
</script>
Paso 7 — Estilo de los indicadores
Un poco de CSS deja el panel legible de un vistazo. El punto verde/rojo es lo que el ojo busca:
.status-painel { max-width: 420px; font-family: system-ui, sans-serif; }
.badge { padding: 3px 10px; border-radius: 4px; font-weight: 700; }
.badge.on { background: #113d1a; color: #35d05a; }
.badge.off { background: #3d1111; color: #ff5c5c; }
.status-lista, #status-lista { list-style: none; padding: 0; }
#status-lista li { padding: 6px 0; display: flex; align-items: center; gap: 8px; }
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.dot.on { background: #35d05a; box-shadow: 0 0 6px #35d05a; }
.dot.off { background: #ff5c5c; }
Paso 8 — Historial de uptime (opcional, pero recomendado)
Un snapshot muestra el "ahora". Para exhibir "99,3% de uptime en los últimos 7 días", graba el resultado periódicamente en una tabla. Configura un cron cada minuto que llame a un script grabador:
<?php
// status/gravar-uptime.php (córrelo vía cron cada 1 minuto)
require_once __DIR__ . '/snapshot.php';
require_once __DIR__ . '/db.php';
$s = gerarSnapshot();
$algumOnline = false;
foreach ($s['servicos'] as $svc) {
if ($svc['online']) { $algumOnline = true; break; }
}
$conn = getMuDB();
sqlsrv_query($conn,
"INSERT INTO StatusHistorico (checado_em, online, jogadores) VALUES (GETDATE(), ?, ?)",
[[$algumOnline ? 1 : 0, SQLSRV_PARAM_IN], [$s['online'], SQLSRV_PARAM_IN]]
);
-- Tabla para el historial (SQL Server)
CREATE TABLE StatusHistorico (
id INT IDENTITY(1,1) PRIMARY KEY,
checado_em DATETIME NOT NULL,
online BIT NOT NULL,
jogadores INT NOT NULL DEFAULT 0
);
GO
-- Uptime de los últimos 7 días (porcentaje)
SELECT
CAST(SUM(CASE WHEN online = 1 THEN 1 ELSE 0 END) AS FLOAT)
/ COUNT(*) * 100 AS uptime_pct
FROM StatusHistorico
WHERE checado_em >= DATEADD(DAY, -7, GETDATE());
En el cron de Linux, la línea sería * * * * * php /var/www/site/status/gravar-uptime.php. En Windows, usa el Programador de Tareas apuntando al php.exe con la ruta del script.
Errores comunes y soluciones
| Síntoma | Causa probable | Solución |
|---|---|---|
| La página se traba por varios segundos | fsockopen sin timeout o con timeout alto | Usa un timeout de 2–3s; activa el caché del snapshot |
| Siempre muestra offline aunque el servidor esté en línea | Firewall bloqueando el puerto desde el host del sitio | Libera el puerto para el IP de origen del sitio |
| Muestra online pero 0 jugadores | Campo de estado de conexión distinto al usado | Ajusta el nombre de la tabla/campo a tu schema |
| Conteo "atascado" tras una caída | El GameServer no puso ConnectStat en cero al arrancar | Pon el campo en cero al boot o usa una tabla de estado propia |
sqlsrv_connect falla de forma intermitente | La página chequea la base en cada request | Activa el caché; maneja la excepción y muestra "DB offline" |
| Latencia siempre alta | Resolución de DNS en el host | Usa el IP directo en vez del hostname en el checker |
Seguridad y buenas prácticas
- Nunca expongas credenciales en el JSON público. El
api.phpsolo debe devolver estado y conteo, jamás cadenas de conexión ni detalles internos. - Restringe los puertos por IP. El puerto administrativo del servidor no debe quedar abierto al mundo solo para que funcione la página — libéralo únicamente para el IP del sitio.
- Maneja la base offline con elegancia. Si
getMuDB()lanza una excepción, muestra "Base en mantenimiento" en vez de un error fatal de PHP que filtre rutas del servidor. - El caché es obligatorio en producción. Sin él, un pico de accesos se vuelve un pico de conexiones TCP y queries en tu GameServer.
- Evita el falso positivo. Un puerto abierto no garantiza que el login funcione. Si puedes, combina la prueba de puerto con el chequeo de la base para un estado "saludable" más honesto.
Lista de verificación de lanzamiento
- Puertos del ConnectServer y del GameServer confirmados en los
.ini - Firewall liberando los puertos desde el host del sitio hacia el del juego
- Función de prueba de puerto con timeout de 2–3 segundos
- Conteo de jugadores validado contra el schema real de la base
- Caché del snapshot activo (TTL 15–30s)
- Endpoint
api.phpretornando JSON sin datos sensibles - Auto-refresh en el front-end (30–60s)
- Manejo elegante de la base offline
- Cron grabando el historial de uptime cada minuto
- Prueba real: tumbar el GameServer y confirmar que pasa a "Offline"
- Prueba real: levantarlo de nuevo y confirmar el retorno a "Online"
Con estos pasos, tu página de estado se vuelve una vitrina confiable del servidor: rápida, resistente a caídas y honesta sobre el estado real de cada servicio. Es el tipo de detalle que separa a un proyecto amateur de un servidor que los jugadores toman en serio.
Preguntas frecuentes
¿La página de estado necesita correr en el mismo servidor del juego?
No obligatoriamente. Puede correr en cualquier host que logre alcanzar por TCP los puertos del ConnectServer y del GameServer, o leer la base. Muchos admins alojan el sitio en un VPS separado y monitorean el servidor de juego por el IP público — solo hay que liberar los puertos en el firewall.
¿Cómo sé qué puerto verificar para el estado online?
El puerto por defecto del ConnectServer suele ser 44405 y el del GameServer 55901, pero eso varía según la versión y según cómo hayas configurado el ConnectServer.ini y el GameServer.ini. Confirma siempre en tus archivos de configuración antes de codear el checker.
¿La prueba de puerto traba la página cuando el servidor está offline?
Se traba si no defines un timeout. Usa siempre un timeout corto (2 a 3 segundos) en fsockopen o stream_socket_client. Sin timeout, un puerto filtrado por el firewall puede dejar la petición colgada por 30 segundos o más.
¿Se pueden contar los jugadores online sin acceder a la memoria del servidor?
Sí. La forma más estable es contar registros en la tabla de personajes o cuentas con el campo de estado de conexión marcado como online (ConnectStat=1 o similar). El nombre exacto del campo varía según la versión del MuServer/IGCN/Season.
¿Con qué frecuencia debo actualizar el estado?
Para el visitante, un auto-refresh cada 30 a 60 segundos es suficiente. En el backend, usa un caché de 15 a 30 segundos para no chequear los puertos en cada request y no sobrecargar el GameServer con consultas.