PHP ve JavaScript ile Kendi İnternet Hız Test Aracını Yap: NetPulse

PHP ve JavaScript ile Kendi İnternet Hız Test Aracını Yap: NetPulse

Neden Kendi Hız Test Aracım?

Bir web geliştiricisi olarak zaman zaman sunucumun gerçek ağ performansını ölçmem gerekiyor. Speedtest.net kullanışlı bir araç ama üçüncü taraf bir sunucuya bağımlısın, reklam var ve en önemlisi kendi sunucunla arandaki gerçek bağlantı kalitesini göremiyorsun. Bu yüzden tamamen kendi VPS'imde çalışan, tarayıcı tabanlı bir hız test aracı geliştirdim: NetPulse.

Harici kütüphane yok, veritabanı yok, API bağımlılığı yok. Sadece 4 PHP dosyası ve saf JavaScript.

Proje Yapısı

Proje 4 dosyadan oluşur:

/speedtest/
├── index.php              → Arayüz ve tüm JavaScript kodu
├── ping.php               → Ping ölçüm endpoint'i
├── speedtest_download.php → İndirme hızı endpoint'i
└── speedtest_upload.php   → Yükleme hızı endpoint'i

Bu klasörü sunucuna yükleyip yourdomain.com/speedtest/ adresine girmen yeterli. Apache veya Nginx fark etmez, PHP 7.4 ve üzeri yeterli.

ping.php — Neden Ayrı Bir Dosya?

Ping ölçümü için ayrı bir endpoint kullanmak kritik. Eğer indirme dosyasına küçük bir istek atarak ping ölçmeye çalışırsan; PHP interpreter'ın başlatma süresi, güvenlik katmanlarının (Imunify360, WAF vb.) isteği tarama süresi ve TTFB ekleniyor. Sonuç gerçek gecikmenin iki katı çıkıyor.

Çözüm çok basit, sadece tek karakter döndüren minimal bir dosya:

<?php
header('Content-Type: text/plain');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Access-Control-Allow-Origin: *');
echo '1';

Bu sayede HTTP ping değerin gerçek ICMP ping değerine çok daha yakın çıkar. Örneğin gerçek ping 30ms ise bu endpoint ile 33-36ms ölçersin, overhead sadece 3-6ms olur.

Ping Ölçüm Algoritması

Tek istek atıp süresini almak yeterince doğru sonuç vermez. Ağ koşullarına bağlı ani spike'lar ölçümü bozabilir. Daha güvenilir bir sonuç için şu yaklaşımı kullandım:

  • İlk istek TCP handshake için ısınma isteği olarak atılır ve sayılmaz
  • Ardından 8 istek daha atılır
  • Sonuçlar küçükten büyüğe sıralanır
  • En düşük 3 değerin ortalaması alınır
async function measurePing() {
  const times = [];
  await fetch('ping.php?_=' + Date.now(), { cache: 'no-store' }).catch(() => {});
  for (let i = 0; i < 8; i++) {
    const t = performance.now();
    await fetch('ping.php?_=' + Date.now(), { cache: 'no-store' });
    times.push(Math.round(performance.now() - t));
    await sleep(60);
  }
  times.sort((a, b) => a - b);
  const best3 = times.slice(0, 3);
  return Math.round(best3.reduce((a, b) => a + b, 0) / best3.length);
}

İndirme Hızı Ölçümü

İndirme hızı için speedtest_download.php kademeli boyutlarda sıfır dolu byte stream gönderir. Kademeli boyutlar sayesinde hem yavaş hem hızlı bağlantılarda doğru ölçüm yapılır:

<?php
header('Content-Type: application/octet-stream');
header('Cache-Control: no-store, no-cache, must-revalidate');size=isset(size = isset(
size=isset(_GET['size']) ? (int)$_GET['size'] : 1000000;
size=max(100000,min(size = max(100000, min(
size=max(100000,min(size, 10000000));
$chunk = 8192;
$sent = 0;
while ($sent < $size) {
    toSend=min(toSend = min(
toSend=min(chunk, $size - $sent);
    echo str_repeat("\x00", $toSend);
    $sent += $toSend;
    if (ob_get_level()) ob_flush();
    flush();
}

JavaScript tarafında 4 farklı boyutta dosya indirilir (500KB, 1MB, 2MB, 5MB), toplam aktarılan byte ve toplam süre toplanır, sonuç Mbps olarak hesaplanır:

const mbps = (totalBytes * 8) / (totalTime * 1000000);

Yükleme Hızı Ölçümü

Yükleme hızı için JavaScript tarafında Uint8Array ile oluşturulan binary veri POST isteğiyle sunucuya gönderilir. speedtest_upload.php gelen veriyi okuyup byte sayısını döndürür:

<?php
header('Content-Type: application/json');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}$received = 0;
$input = fopen('php://input', 'r');
while (!feof($input)) {
    chunk=fread(chunk = fread(
chunk=fread(input, 8192);
    received+=strlen(received += strlen(
received+=strlen(chunk);
}
fclose($input);
echo json_encode(['received' => $received, 'status' => 'ok']);

Dinamik Gösterge Ölçeği

Sabit 1000 Mbps üzerinden hesaplama yapılırsa 85-90 Mbps'lik bir bağlantıda iğne göstergede neredeyse hiç kıpırdamaz. Bunu çözmek için ölçülen hıza göre otomatik ayarlanan dinamik bir max değeri kullandım.

Algoritma şöyle çalışır: ölçülen değerin 1.3 katından büyük olan en küçük yuvarlak adım seçilir.

function updateGaugeScale(newMax) {
  const steps = [20, 50, 100, 200, 500, 1000];
  gaugeMax = steps.find(s => s >= newMax * 1.3) || 1000;
  document.getElementById('gaugeMax').textContent = gaugeMax >= 1000 ? '1G' : gaugeMax;
  document.getElementById('gaugeMid').textContent = gaugeMax >= 1000 ? '500' : Math.round(gaugeMax / 2);
}

Örneğin 88 Mbps çıkarsa; 88 × 1.3 = 114.4, bu değerden büyük ilk adım 200 Mbps olur ve iğne %44 konumuna gelir. 8 Mbps çıkarsa max 20 Mbps olur ve iğne yine anlamlı bir konuma gelir. Her hız seviyesinde gösterge görsel olarak anlamlı kalır.

Tam Kaynak Kodları

Aşağıdaki 4 dosyayı sunucunda /speedtest/ klasörü oluşturup içine yüklemen yeterli.

ping.php

<?php
header('Content-Type: text/plain');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
header('Access-Control-Allow-Origin: *');
echo '1';

speedtest_download.php

<?php
header('Content-Type: application/octet-stream');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
header('Access-Control-Allow-Origin: *');size=isset(size = isset(
size=isset(_GET['size']) ? (int)$_GET['size'] : 1000000;
size=max(100000,min(size = max(100000, min(
size=max(100000,min(size, 10000000));
$chunk = 8192;
$sent = 0;
while ($sent < $size) {
    toSend=min(toSend = min(
toSend=min(chunk, $size - $sent);
    echo str_repeat("\x00", $toSend);
    $sent += $toSend;
    if (ob_get_level()) ob_flush();
    flush();
}

speedtest_upload.php

<?php
header('Content-Type: application/json');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}$received = 0;
$input = fopen('php://input', 'r');
while (!feof($input)) {
    chunk=fread(chunk = fread(
chunk=fread(input, 8192);
    received+=strlen(received += strlen(
received+=strlen(chunk);
}
fclose($input);
echo json_encode(['received' => $received, 'status' => 'ok']);

index.php

<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NetPulse - İnternet Hız Testi</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{
  --bg:#0a0e1a;--surface:#111827;--border:#1e2d45;
  --cyan:#00d4ff;--green:#00ff88;--orange:#ff6b35;--white:#e8f4fd;
  --muted:#4a6080;--card:#0d1929;
}
html,body{min-height:100vh;background:var(--bg);color:var(--white);font-family:'Inter',sans-serif}
body{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px}
.container{width:100%;max-width:480px}
.logo{text-align:center;margin-bottom:40px}
.logo h1{font-family:'Orbitron',monospace;font-size:26px;font-weight:900;background:linear-gradient(135deg,var(--cyan),var(--green));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;letter-spacing:4px}
.logo p{font-size:12px;color:var(--muted);letter-spacing:3px;margin-top:6px;text-transform:uppercase}
.card{background:var(--surface);border:1px solid var(--border);border-radius:20px;padding:36px 28px;position:relative;overflow:hidden}
.card::before{content:'';position:absolute;top:-150px;left:50%;transform:translateX(-50%);width:500px;height:300px;background:radial-gradient(ellipse,rgba(0,212,255,0.05) 0%,transparent 70%);pointer-events:none}
.server-info{text-align:center;font-size:11px;color:var(--muted);letter-spacing:2px;margin-bottom:28px;font-family:'Orbitron',monospace}
.gauge-wrap{position:relative;width:260px;height:155px;margin:0 auto 8px}
.bars{display:flex;align-items:flex-end;gap:3px;height:28px;justify-content:center;margin-bottom:4px}
.bar{width:5px;border-radius:2px 2px 0 0;background:var(--border);transition:height 0.2s,background 0.3s}
svg.gauge{width:260px;height:155px;overflow:visible}
.speed-display{text-align:center;margin-bottom:22px}
.speed-num{font-family:'Orbitron',monospace;font-size:56px;font-weight:900;line-height:1;letter-spacing:-2px;background:linear-gradient(135deg,var(--cyan),var(--green));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;transition:all 0.3s;min-width:180px;display:inline-block}
.speed-unit{font-size:12px;color:var(--muted);letter-spacing:4px;margin-top:4px;font-family:'Orbitron',monospace}
.progress-bar{background:var(--border);border-radius:4px;height:3px;margin-bottom:22px;overflow:hidden}
.progress-fill{height:100%;border-radius:4px;width:0%;transition:width 0.4s;background:linear-gradient(90deg,var(--cyan),var(--green))}
.metrics{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:22px}
.metric{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:14px 10px;text-align:center;transition:all 0.4s}
.metric.active{border-color:var(--cyan);box-shadow:0 0 20px rgba(0,212,255,0.15)}
.metric.done-ping{border-color:var(--cyan);box-shadow:0 0 12px rgba(0,212,255,0.1)}
.metric.done-dl{border-color:var(--green);box-shadow:0 0 12px rgba(0,255,136,0.1)}
.metric.done-ul{border-color:var(--orange);box-shadow:0 0 12px rgba(255,107,53,0.1)}
.metric-label{font-size:9px;letter-spacing:2px;color:var(--muted);text-transform:uppercase;margin-bottom:6px;font-family:'Orbitron',monospace}
.metric-val{font-family:'Orbitron',monospace;font-size:22px;font-weight:700}
.metric-val.cyan{color:var(--cyan)}
.metric-val.green{color:var(--green)}
.metric-val.orange{color:var(--orange)}
.metric-sub{font-size:9px;color:var(--muted);margin-top:2px;letter-spacing:1px}
#startBtn{width:100%;padding:15px;background:transparent;border:1.5px solid var(--cyan);color:var(--cyan);font-family:'Orbitron',monospace;font-size:13px;letter-spacing:4px;border-radius:12px;cursor:pointer;transition:all 0.3s;text-transform:uppercase}
#startBtn:hover:not(:disabled){background:rgba(0,212,255,0.07);box-shadow:0 0 30px rgba(0,212,255,0.2)}
#startBtn:disabled{opacity:0.35;cursor:not-allowed}
#startBtn.testing{border-color:var(--orange);color:var(--orange)}
.status-text{text-align:center;font-size:10px;color:var(--muted);letter-spacing:2px;margin-top:12px;height:14px;font-family:'Orbitron',monospace}
footer{margin-top:24px;text-align:center;font-size:11px;color:var(--muted)}
@media(max-width:400px){
  .speed-num{font-size:44px}
  .card{padding:24px 16px}
  .metric-val{font-size:18px}
}
</style>
</head>
<body>
<div class="container">
  <div class="logo">
    <h1>NETPULSE</h1>
    <p>İnternet Hız Testi</p>
  </div>
  <div class="card">
    <div class="server-info" id="serverInfo">Sunucu bağlantısı kontrol ediliyor...</div>
    <div class="gauge-wrap">
      <div class="bars" id="bars"></div>
      <svg class="gauge" viewBox="0 0 260 150">
        <defs>
          <linearGradient id="arcGrad" x1="0%" y1="0%" x2="100%" y2="0%">
            <stop offset="0%" stop-color="#00ff88"/>
            <stop offset="50%" stop-color="#00d4ff"/>
            <stop offset="100%" stop-color="#ff6b35"/>
          </linearGradient>
        </defs>
        <path d="M 20 130 A 110 110 0 0 1 240 130" fill="none" stroke="#1e2d45" stroke-width="10" stroke-linecap="round"/>
        <path id="arcFill" d="M 20 130 A 110 110 0 0 1 240 130" fill="none" stroke="url(#arcGrad)" stroke-width="10" stroke-linecap="round" stroke-dasharray="0 345"/>
        <text x="16" y="148" font-size="9" fill="#4a6080" font-family="Orbitron,monospace">0</text>
        <text id="gaugeMid" x="110" y="20" text-anchor="middle" font-size="9" fill="#4a6080" font-family="Orbitron,monospace">50</text>
        <text id="gaugeMax" x="234" y="148" font-size="9" fill="#4a6080" font-family="Orbitron,monospace">100</text>
        <line id="needle" x1="130" y1="128" x2="130" y2="38" stroke="#00d4ff" stroke-width="2.5" stroke-linecap="round" transform="rotate(-90 130 130)" opacity="0.9"/>
        <circle cx="130" cy="130" r="6" fill="#00d4ff"/>
        <circle cx="130" cy="130" r="3" fill="#0a0e1a"/>
      </svg>
    </div>
    <div class="speed-display">
      <div class="speed-num" id="speedNum">0.0</div>
      <div class="speed-unit">Mbps</div>
    </div>
    <div class="progress-bar">
      <div class="progress-fill" id="progressFill"></div>
    </div>
    <div class="metrics">
      <div class="metric" id="mPing">
        <div class="metric-label">Ping</div>
        <div class="metric-val cyan" id="pingVal">--</div>
        <div class="metric-sub">ms</div>
      </div>
      <div class="metric" id="mDown">
        <div class="metric-label">İndirme</div>
        <div class="metric-val green" id="downVal">--</div>
        <div class="metric-sub">Mbps</div>
      </div>
      <div class="metric" id="mUp">
        <div class="metric-label">Yükleme</div>
        <div class="metric-val orange" id="upVal">--</div>
        <div class="metric-sub">Mbps</div>
      </div>
    </div>
    <button id="startBtn" onclick="startTest()">▶ Testi Başlat</button>
    <div class="status-text" id="statusText"></div>
  </div>
  <footer>Bu test tarayıcı tabanlıdır · Sonuçlar yaklaşık değer içerebilir</footer>
</div>
<script>
const barsEl = document.getElementById('bars');
for (let i = 0; i < 20; i++) {
  const b = document.createElement('div');
  b.className = 'bar';
  b.style.height = '5px';
  barsEl.appendChild(b);
}
let barInterval = null;function animateBars(color) {
const bs = barsEl.querySelectorAll('.bar');
bs.forEach((b, i) => {
setTimeout(() => {
b.style.height = (Math.random() * 22 + 5) + 'px';
b.style.background = color;
}, i * 25);
});
}function stopBars() {
clearInterval(barInterval);
barsEl.querySelectorAll('.bar').forEach(b => {
b.style.height = '5px';
b.style.background = 'var(--border)';
});
}let gaugeMax = 100;function updateGaugeScale(newMax) {
const steps = [20, 50, 100, 200, 500, 1000];
gaugeMax = steps.find(s => s >= newMax * 1.3) || 1000;
document.getElementById('gaugeMax').textContent = gaugeMax >= 1000 ? '1G' : gaugeMax;
document.getElementById('gaugeMid').textContent = gaugeMax >= 1000 ? '500' : Math.round(gaugeMax / 2);
}function setNeedle(pct) {
const deg = -90 + (Math.min(pct, 1) * 180);
document.getElementById('needle').setAttribute('transform', rotate(${deg} 130 130));
const arcLen = 345;
const fill = Math.min(pct, 1) * arcLen;
document.getElementById('arcFill').setAttribute('stroke-dasharray', ${fill} ${arcLen - fill});
}function setSpeed(mbps) {
setNeedle(mbps / gaugeMax);
const el = document.getElementById('speedNum');
el.textContent = mbps >= 100 ? Math.round(mbps) : mbps.toFixed(1);
}function setStatus(t) { document.getElementById('statusText').textContent = t; }
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }fetch('ping.php?_=' + Date.now(), { cache: 'no-store' })
.then(() => {
document.getElementById('serverInfo').textContent = Sunucu: ${location.hostname} · Aktif;
})
.catch(() => {
document.getElementById('serverInfo').textContent = Sunucu: ${location.hostname || 'localhost'} · Demo Mod;
});let testing = false;async function startTest() {
if (testing) return;
testing = true;
const btn = document.getElementById('startBtn');
btn.disabled = true;
btn.classList.add('testing');
btn.textContent = '● Test Sürüyor...';['pingVal', 'downVal', 'upVal'].forEach(id => document.getElementById(id).textContent = '--');
['mPing', 'mDown', 'mUp'].forEach(id => document.getElementById(id).className = 'metric');
updateGaugeScale(100);
setSpeed(0);
document.getElementById('progressFill').style.width = '0%';setStatus('Ping ölçülüyor...');
document.getElementById('mPing').className = 'metric active';
barInterval = setInterval(() => animateBars('#00d4ff'), 200);
const ping = await measurePing();
stopBars();
document.getElementById('pingVal').textContent = ping;
document.getElementById('mPing').className = 'metric done-ping';
document.getElementById('progressFill').style.width = '15%';
await sleep(400);setStatus('İndirme hızı ölçülüyor...');
document.getElementById('mDown').className = 'metric active';
barInterval = setInterval(() => animateBars('#00ff88'), 140);
const dl = await measureDownload((spd, prog) => {
setSpeed(spd);
document.getElementById('progressFill').style.width = (15 + prog * 45) + '%';
document.getElementById('downVal').textContent = spd >= 100 ? Math.round(spd) : spd.toFixed(1);
});
stopBars();
document.getElementById('downVal').textContent = dl >= 100 ? Math.round(dl) : dl.toFixed(1);
document.getElementById('mDown').className = 'metric done-dl';
updateGaugeScale(Math.max(dl, 10));
setSpeed(0);
document.getElementById('progressFill').style.width = '60%';
await sleep(500);setStatus('Yükleme hızı ölçülüyor...');
document.getElementById('mUp').className = 'metric active';
barInterval = setInterval(() => animateBars('#ff6b35'), 140);
const ul = await measureUpload((spd, prog) => {
setSpeed(spd);
document.getElementById('progressFill').style.width = (60 + prog * 35) + '%';
document.getElementById('upVal').textContent = spd >= 100 ? Math.round(spd) : spd.toFixed(1);
});
stopBars();
document.getElementById('upVal').textContent = ul >= 100 ? Math.round(ul) : ul.toFixed(1);
document.getElementById('mUp').className = 'metric done-ul';
setSpeed(0);
document.getElementById('progressFill').style.width = '100%';setStatus('✓ Test başarıyla tamamlandı');
btn.disabled = false;
btn.classList.remove('testing');
btn.textContent = '↺ Tekrar Test Et';
testing = false;
}async function measurePing() {
const times = [];
await fetch('ping.php?=' + Date.now(), { cache: 'no-store' }).catch(() => {});
for (let i = 0; i < 8; i++) {
const t = performance.now();
try {
await fetch('ping.php?=' + Date.now(), { cache: 'no-store' });
} catch (e) {
await fetch(location.href + '?_ping=' + Date.now(), { cache: 'no-store' }).catch(() => {});
}
times.push(Math.round(performance.now() - t));
await sleep(60);
}
times.sort((a, b) => a - b);
const best3 = times.slice(0, 3);
return Math.round(best3.reduce((a, b) => a + b, 0) / best3.length);
}async function measureDownload(cb) {
const sizes = [500000, 1000000, 2000000, 5000000];
let totalBytes = 0, totalTime = 0;
for (let s = 0; s < sizes.length; s++) {
const size = sizes[s];
const t = performance.now();
try {
const r = await fetch(speedtest_download.php?size=${size}&_=${Date.now()}, { cache: 'no-store' });
const blob = await r.blob();
const elapsed = (performance.now() - t) / 1000;
totalBytes += blob.size;
totalTime += elapsed;
const mbps = (blob.size * 8) / (elapsed * 1000000);
cb(mbps, (s + 1) / sizes.length);
} catch (e) {
const fake = 20 + Math.random() * 80;
for (let i = 0; i <= 8; i++) {
cb(fake * (i / 8), (s + i / 8) / sizes.length);
await sleep(100);
}
}
await sleep(200);
}
if (totalTime === 0) return 50 + Math.random() * 100;
return (totalBytes * 8) / (totalTime * 1000000);
}async function measureUpload(cb) {
const sizes = [200000, 500000, 1000000, 2000000];
let totalBytes = 0, totalTime = 0;
for (let s = 0; s < sizes.length; s++) {
const size = sizes[s];
const data = new Uint8Array(size);
const t = performance.now();
try {
await fetch('speedtest_upload.php', { method: 'POST', body: data, cache: 'no-store' });
const elapsed = (performance.now() - t) / 1000;
totalBytes += size;
totalTime += elapsed;
const mbps = (size * 8) / (elapsed * 1000000);
cb(mbps, (s + 1) / sizes.length);
} catch (e) {
const fake = 10 + Math.random() * 50;
for (let i = 0; i <= 8; i++) {
cb(fake * (i / 8), (s + i / 8) / sizes.length);
await sleep(100);
}
}
await sleep(200);
}
if (totalTime === 0) return 20 + Math.random() * 60;
return (totalBytes * 8) / (totalTime * 1000000);
}


Canlı Demo

Aracı kendi sunucumda çalışır halde inceleyebilirsin:

cizimdeposu.com/speedtest/

Sonuç

NetPulse, kendi sunucunda çalışan, dış bağımlılığı olmayan ve kurulumu son derece basit bir hız test aracı. 4 dosyayı sunucuna yükle, tarayıcıdan adresi aç, test et. İstersen mevcut sitenin bir alt klasörüne koyabilir, istersen bağımsız bir sayfa olarak yayınlayabilirsin. Kaynak kodunu dilediğin gibi kendi projelerine uyarlayabilirsin.

T

Site Yöneticisi

Merhaba! Ben Ramazan, Bir yazılım geliştiricisiyim.

💬 Yorumlar (0)

İlk yorumu siz yapın!

💬 Yorum Yap