使用docker部署Speedtest-X内网测试网站

最终效果

部署过程

  • 准备服务器一台,并且接入公司网络
  • 部署speedtestx docker镜像并启动
  • 对speedtestx原有静态页面进行改造并上线

部署镜像

bash 复制代码
# 拉取镜像
docker pull docker.io/badapple9/speedtest-x:2024-10-03
yaml 复制代码
# 创建docker-compose
# 并且将静态文件挂载到宿主机

cat > docker-compose.yaml << 'EOF'
version: '3.9'
services:
    speedtest-x:
        image: speedtest-x:2024-10-03
        tty: true
        stdin_open: true
        ports:
            - '5000:80'
        restart: always
        volumes:
            - ./html:/var/www/html
            - ./backend:/var/www/backend
        container_name: speedtest-x
EOF
bash 复制代码
# 后台运行配置
docker compose up -d

修改静态页面

html/index.html 以及 html/backend/getIP.php 两个文件

xml 复制代码
<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
	<title>XX测速</title>
	<link rel="shortcut icon" href="favicon.ico">
	<script type="text/javascript" src="speedtest.js"></script>
	<script type="text/javascript">

		//INITIALIZE SPEEDTEST
		var s = new Speedtest(); //create speedtest object
		var xhr = new XMLHttpRequest();
		var url_report = './backend/report.php';
		var milestone = 0;
		var key_prefix = Date.parse(new Date());
		s.onupdate = function (data) { //callback to update data in UI
			var clientIp = data.clientIp || "获取失败";
			// 解析IP地址和位置信息
			var ipIspArr = clientIp.split(' - ', 3);
			var ip = ipIspArr[0];
			var location = ipIspArr.slice(1).join(' - ');

			// 更新IP地址显示,添加样式
			if (location) {
				I("ip").innerHTML = '<span class="ip-address">' + ip + '</span> - <span class="ip-info">' + location + '</span>';
			} else {
				I("ip").innerHTML = '<span class="ip-address">' + ip + '</span>';
			}

			I("dlText").textContent = (data.testState == 1 && data.dlStatus == 0) ? "..." : data.dlStatus;
			I("ulText").textContent = (data.testState == 3 && data.ulStatus == 0) ? "..." : data.ulStatus;
			I("pingText").textContent = data.pingStatus;
			I("jitText").textContent = data.jitterStatus;
			var prog = (Number(data.dlProgress) * 2 + Number(data.ulProgress) * 2 + Number(data.pingProgress)) / 5;
			I("progress").style.width = (100 * prog) + "%";
			var isp = ipIspArr[1];
			var addr = ipIspArr[2] === undefined ? '' : ipIspArr[2];
			var progress = Math.floor(100 * prog);
			var key = key_prefix + "_" + ip;
			if (progress > 20 && (progress % 10 == 0) && progress != milestone) {
				console.log(progress);
				var params = 'key=' + key + '&ip=' + ip + '&isp=' + isp + '&addr=' + addr + '&dspeed=' + I("dlText").textContent + '&uspeed=' + I("ulText").textContent + '&ping=' + I("pingText").textContent
					+ '&jitter=' + I("jitText").textContent;
				xhr.timeout = 3000;
				xhr.ontimeout = function (e) {
					console.log('上报超时');
				};
				xhr.open('POST', url_report, true);
				xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
				xhr.send(params);
				milestone = progress;
			}
		}
		s.onend = function (aborted) { //callback for test ended/aborted
			I("startStopBtn").className = ""; //show start button again
			if (aborted) { //if the test was aborted, clear the UI and prepare for new test
				initUI();
			}
		}

		function startStop() { //start/stop button pressed
			if (s.getState() == 3) {
				//speedtest is running, abort
				s.abort();
			} else {
				//test is not running, begin
				s.start();
				I("startStopBtn").className = "running";
			}
		}

		//function to (re)initialize UI
		function initUI() {
			I("dlText").textContent = "";
			I("ulText").textContent = "";
			I("pingText").textContent = "";
			I("jitText").textContent = "";
			I("ip").textContent = "";
		}

		function I(id) { return document.getElementById(id); }
	</script>

	<style type="text/css">
		html,
		body {
			border: none;
			padding: 0;
			margin: 0;
			/* background: #F5F5F7; */
			color: #1D1D1F;
		}

		body {
			width: 100vw;
			height: 100vh;
			background:
				/* 从右上角到左下角的渐变遮罩 - 逐渐隐藏 */
				linear-gradient(180deg,
					rgba(255, 255, 255, 1) 0%,
					rgba(255, 255, 255, 0.95) 25%,
					rgba(255, 255, 255, 0.85) 50%,
					rgba(255, 255, 255, 0.7) 75%,
					rgba(255, 255, 255, 0.5) 100%),
				/* 网格线条 */
				linear-gradient(to left, rgba(0, 0, 0, 0.2) 1px, transparent 1px),
				linear-gradient(to top, rgba(0, 0, 0, 0.2) 1px, transparent 1px);
			background-size:
				cover,
				20px 20px,
				20px 20px;
			background-color: white;
			overflow: hidden;
			text-align: center;
			font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
		}

		h1 {
			color: #1D1D1F;
			font-weight: 600;
			letter-spacing: -0.02em;
			margin-top: 2em;
			margin-bottom: 1.5em;
		}

		#startStopBtn {
			display: inline-block;
			margin: 0 auto;
			color: #FFFFFF;
			background-color: #0071E3;
			border: none;
			border-radius: 12px;
			transition: all 0.2s ease;
			box-sizing: border-box;
			width: 10em;
			height: 3.2em;
			line-height: 3.2em;
			cursor: pointer;
			font-weight: 500;
			font-size: 16px;
			box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
		}

		#startStopBtn:hover {
			background-color: #0077ED;
			transform: translateY(-1px);
			box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
		}

		#startStopBtn:active {
			background-color: #0066CC;
			transform: translateY(0);
			box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
		}

		#startStopBtn.running {
			background-color: #FF3B30;
		}

		#startStopBtn.running:hover {
			background-color: #FF453A;
		}

		#startStopBtn.running:active {
			background-color: #FF3B30;
		}

		#startStopBtn:before {
			content: "开始测速";
		}

		#startStopBtn.running:before {
			content: "停止测试";
		}

		#test {
			margin-top: 3em;
			margin-bottom: 6em;
			max-width: 900px;
			margin-left: auto;
			margin-right: auto;
			padding: 0 20px;
		}

		.testContainer {
			display: flex;
			flex-wrap: wrap;
			justify-content: center;
			align-items: center;
			gap: 20px;
			margin-bottom: 2em;
		}

		div.testArea {
			flex: 1 1 200px;
			max-width: 220px;
			height: 180px;
			position: relative;
			box-sizing: border-box;
			background: rgb(255, 255, 255, 0);
			border-radius: 18px;
			padding: 24px;
			box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
			transition: all 0.3s ease;
			display: flex;
			flex-direction: column;
			justify-content: space-between;
			align-items: center;
			text-align: center;
		}

		div.testArea:hover {
			transform: translateY(-2px);
			box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
		}

		div.testName {
			font-size: 14px;
			font-weight: 500;
			color: #86868B;
			margin-bottom: 16px;
		}

		div.meterText {
			font-size: 32px;
			font-weight: 600;
			color: #1D1D1F;
			line-height: 1;
			margin: 8px 0;
		}

		#dlText {
			color: #0071E3;
		}

		#ulText {
			color: #34C759;
		}

		#pingText,
		#jitText {
			color: #FF9500;
		}

		div.meterText:empty:before {
			color: #D2D2D7 !important;
			content: "0.00";
		}

		div.unit {
			font-size: 12px;
			color: #86868B;
			margin-top: 8px;
		}


		@media all and (max-width:65em) {
			body {
				font-size: 1.5vw;
			}
		}

		@media all and (max-width:40em) {
			body {
				font-size: 0.9em;
			}

			h1 {
				font-size: 1.8em;
				margin-top: 1.5em;
				margin-bottom: 1.2em;
			}

			#startStopBtn {
				width: 12em;
				height: 3.5em;
				line-height: 3.5em;
				font-size: 14px;
			}

			#test {
				margin-top: 2em;
				margin-bottom: 4em;
				padding: 0 16px;
			}

			.testContainer {
				flex-direction: row;
				flex-wrap: wrap;
				justify-content: center;
				align-items: center;
				gap: 12px;
			}

			div.testArea {
				flex: 1 1 calc(50% - 6px);
				max-width: 180px;
				height: 150px;
				margin: 0;
				padding: 16px;
			}

			div.testName {
				font-size: 13px;
			}

			div.meterText {
				font-size: 24px;
			}

			div.unit {
				font-size: 11px;
			}

			#ipArea {
				font-size: 13px;
				padding: 0 16px;
				margin-top: 1.5em;
			}

			p a {
				font-size: 13px;
				padding: 0 16px;
				display: inline-block;
			}
		}

		@media all and (max-width:320px) {
			h1 {
				font-size: 1.6em;
			}

			div.testArea {
				flex: 1 1 calc(50% - 6px);
				max-width: 140px;
				height: 140px;
				padding: 14px;
			}

			div.testName {
				font-size: 12px;
			}

			div.meterText {
				font-size: 22px;
			}

			div.unit {
				font-size: 10px;
			}
		}

		#progressBar {
			width: 90%;
			height: 6px;
			background-color: #E5E5EA;
			position: relative;
			display: block;
			margin: 0 auto;
			margin-bottom: 2.5em;
			border-radius: 3px;
		}

		#progress {
			position: absolute;
			top: 0;
			left: 0;
			height: 100%;
			width: 0%;
			transition: width 0.3s ease;
			background-color: #0071E3;
			border-radius: 3px;
		}

		#ipArea {
			margin-top: 2em;
			font-size: 14px;
			color: #86868B;
			background: #FFFFFF;
			border-radius: 16px;
			padding: 20px;
			box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
			/* max-width: 600px; */
			margin-left: auto;
			margin-right: auto;
			display: flex;
			align-items: center;
			justify-content: center;
			flex-wrap: wrap;
			gap: 8px;
		}


		#ip {
			word-break: break-all;
		}

		#ipArea .ip-label {
			color: #86868B;
			font-weight: 500;
		}

		#ipArea .ip-info {
			color: #0071E3;
			font-weight: 500;
		}

		#ipArea .ip-address {
			color: #1D1D1F;
			font-weight: 600;
			font-size: 14px;
		}

		p a {
			color: #0071E3;
			text-decoration: none;
			font-size: 14px;
			font-weight: 500;
			transition: color 0.2s ease;
		}

		p a:hover {
			color: #0066CC;
			text-decoration: underline;
		}

		@media (prefers-color-scheme: dark) {

			html,
			body {
				background: #1C1C1E;
				color: #F5F5F7;
			}

			h1 {
				color: #F5F5F7;
			}

			div.testArea {
				background: #2C2C2E;
				box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
			}

			div.testArea:hover {
				box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
			}

			div.testName {
				color: #86868B;
			}

			div.meterText {
				color: #F5F5F7;
			}

			div.unit {
				color: #86868B;
			}

			#progressBar {
				background-color: #3A3A3C;
			}

			#progress {
				background-color: #0071E3;
			}

			#ipArea {
				color: #86868B;
				background: #2C2C2E;
				box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
				border-left: 4px solid #0071E3;
			}

			#ipArea .ip-label {
				color: #86868B;
			}

			#ipArea .ip-info {
				color: #34C759;
			}

			#ipArea .ip-address {
				color: #F5F5F7;
			}

			p a {
				color: #34C759;
			}

			p a:hover {
				color: #30B053;
			}


		}
	</style>
</head>

<body>
	<h1>XX大厦内网测速平台</h1>
	<div id="startStopBtn" onclick="startStop()"></div>
	<div id="test">
		<div id="progressBar">
			<div id="progress"></div>
		</div>
		<div class="testContainer">
			<div class="testArea">
				<div class="testName">下载速度</div>
				<div id="dlText" class="meterText"></div>
				<div class="unit">Mbps</div>
			</div>
			<div class="testArea">
				<div class="testName">上传速度</div>
				<div id="ulText" class="meterText"></div>
				<div class="unit">Mbps</div>
			</div>
			<div class="testArea">
				<div class="testName">延迟</div>
				<div id="pingText" class="meterText"></div>
				<div class="unit">ms</div>
			</div>
			<div class="testArea">
				<div class="testName">抖动</div>
				<div id="jitText" class="meterText"></div>
				<div class="unit">ms</div>
			</div>
		</div>
		<div id="ipArea">
			<span class="ip-label"></span>IP地址:</span>
			<span id="ip"></span>
		</div>
	</div>
	<p><a href="http://10.10.10.2:8000/portal/dingtalk/index.html" target="_blank">如果您访问网络受限,请点击此链接</a></p>
	<script type="text/javascript">
		initUI();
	</script>
</body>

</html>
kotlin 复制代码
# 只修改部分

function getLocalOrPrivateIpInfo($ip)
{
    // ::1/128 is the only localhost ipv6 address. there are no others, no need to strpos this
    if ('::1' === $ip) {
        return '本地 IPv6 地址';
    }

    // simplified IPv6 link-local address (should match fe80::/10)
    if (stripos($ip, 'fe80:') === 0) {
        return '链路本地 IPv6 地址';
    }

    // anything within the 127/8 range is localhost ipv4, the ip must start with 127.0
    if (strpos($ip, '127.') === 0) {
        return '本地 IPv4 地址';
    }

    // 10/8 private IPv4
    if (strpos($ip, '10.') === 0) {
        return '私有 IPv4 地址';
    }

    // 172.16/12 private IPv4
    if (preg_match('/^172\.(1[6-9]|2\d|3[01])\./', $ip) === 1) {
        // 172.16.30.0/24 属于3/5楼用户地址
        if (preg_match('/^172\.16\.30\./', $ip) === 1) {
            return '3&5楼终端设备';
        }
        // 172.16.40.0/24 属于机房服务器地址
        if (preg_match('/^172\.16\.40\./', $ip) === 1) {
            return '机房服务器设备';
        }
        // 172.16.20.0/24 属于4楼用户地址
        if (preg_match('/^172\.16\.20\./', $ip) === 1) {
            return '4楼终端设备';
        }
        // 172.16.103.0-172.16.104.254 属于云桌面地址
        if (preg_match('/^172\.16\.(103|104)\./', $ip) === 1) {
            return '云桌面虚拟机设备';
        }
        // 172.16.80.0/23 属于1-2楼用户地址
        if (preg_match('/^172\.16\.(80|81)\./', $ip) === 1) {
            return '1&2楼终端设备';
        }
        return '私有 IPv4 地址';
    }

    // 192.168/16 private IPv4
    if (strpos($ip, '192.168.') === 0) {
        return '私有 IPv4 地址';
    }

    // IPv4 link-local
    if (strpos($ip, '169.254.') === 0) {
        return '链路本地 IPv4 地址';
    }

    return null;
}
相关推荐
❀͜͡傀儡师3 小时前
docker部署Apache Answer 一款高效问答平台
docker·容器·apache
C_心欲无痕4 小时前
Docker 核心概念和安装
运维·docker·容器
Tummer83635 小时前
Docker迁移(N8N项目)
docker·容器
陈平安Java and C5 小时前
Docker Compose容器编排
docker
江湖有缘6 小时前
Docker一键部署docat:打造轻量级开源文档管理系统
docker·容器·开源
Tummer83636 小时前
Docker+n8n全流程配置和部署(N8N部署流程)
运维·docker·容器
Lam㊣6 小时前
Centos 7 系统docker pull 设置代理
docker·eureka·centos
程序员老赵6 小时前
PyTorch Docker 容器化部署与生产运行实践
pytorch·docker·容器
lewis_lk6 小时前
docker-compose部署mysql&redis
后端·docker