共享主机环境下PHP TCP Socket Server开发需求(适配ESP32)
PHP TCP Socket服务器完善方案(支持HTML页面发消息)
一、修复并优化TCP服务器代码
先修正原代码中的重复函数问题,同时调整客户端存储方式为关联数组,用客户端地址作为键、连接资源作为值,方便后续精准定位发送对象,同时实现10个客户端的连接限制:
<?php error_reporting(E_ALL); set_time_limit(0); // 禁用脚本超时,适配共享主机环境 $host = '0.0.0.0'; $port = 8085; $max_clients = 10; // 限制最大连接数 $clients = []; // 关联数组:key=客户端地址,value=连接资源 $onlineUsersFile = 'online_users.json'; $messageQueueFile = 'message_queue.json'; // 消息队列文件,用于中转Web页面的发送请求 $server = stream_socket_server("tcp://$host:$port", $errno, $errstr); if (!$server) { die("Error: $errstr ($errno)\n"); } echo "Server started on $host:$port\n"; // 初始化消息队列文件 if (!file_exists($messageQueueFile)) { file_put_contents($messageQueueFile, json_encode([])); } while (true) { $read = array_values($clients); $read[] = $server; if (!stream_select($read, $write, $except, 1)) { continue; } // 处理新连接 if (in_array($server, $read)) { if (count($clients) >= $max_clients) { $newClient = stream_socket_accept($server); stream_socket_sendto($newClient, "Server is full, try later.\n"); fclose($newClient); echo "Rejected new client - max connections reached\n"; continue; } $newClient = stream_socket_accept($server); socket_set_blocking($newClient, 0); $clientAddr = stream_socket_get_name($newClient, true); $clients[$clientAddr] = $newClient; stream_socket_sendto($newClient, "Welcome to the server!\n"); echo "New client connected: $clientAddr\n"; broadcastToAll($clients, "New client joined: $clientAddr\n"); // 移除服务器在read数组中的条目 $key = array_search($server, $read); unset($read[$key]); } // 处理客户端消息或断开 foreach ($read as $client) { stream_set_timeout($client, 1); $data = fread($client, 1024); $clientAddr = stream_socket_get_name($client, true); if ($data === false || feof($client)) { fclose($client); unset($clients[$clientAddr]); echo "Client disconnected: $clientAddr\n"; broadcastToAll($clients, "Client left: $clientAddr\n"); continue; } if (!empty(trim($data))) { echo "Received from $clientAddr: $data"; broadcast($client, "[$clientAddr] $data", $clients); } } // 处理Web页面发来的消息队列 $queue = json_decode(file_get_contents($messageQueueFile), true) ?? []; if (!empty($queue)) { foreach ($queue as $index => $msgItem) { if (isset($clients[$msgItem['target']])) { stream_socket_sendto($clients[$msgItem['target']], "Admin: {$msgItem['message']}"); echo "Sent to {$msgItem['target']}: {$msgItem['message']}"; } unset($queue[$index]); } file_put_contents($messageQueueFile, json_encode(array_values($queue))); } updateOnlineUsers($clients, $onlineUsersFile); usleep(10000); // 降低CPU占用 } // 向除发送者外的所有客户端广播 function broadcast($sender, $message, $clients) { $senderAddr = stream_socket_get_name($sender, true); foreach ($clients as $addr => $client) { if ($addr !== $senderAddr) { stream_socket_sendto($client, $message); } } } // 向所有客户端广播 function broadcastToAll($clients, $message) { foreach ($clients as $client) { stream_socket_sendto($client, $message); } } // 更新在线用户列表到JSON文件 function updateOnlineUsers($clients, $file) { $onlineUsers = array_keys($clients); file_put_contents($file, json_encode($onlineUsers)); } ?>
二、创建消息中转接口(send_message.php)
这个文件负责接收HTML页面的请求,要么返回在线客户端列表,要么将发送请求写入消息队列:
<?php $onlineUsersFile = 'online_users.json'; $messageQueueFile = 'message_queue.json'; // 初始化队列文件 if (!file_exists($messageQueueFile)) { file_put_contents($messageQueueFile, json_encode([])); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $clientAddr = $_POST['client_addr'] ?? ''; $message = $_POST['message'] ?? ''; if (empty($clientAddr) || empty($message)) { echo json_encode(['status' => 'error', 'msg' => 'Missing parameters']); exit; } // 将消息写入队列 $queue = json_decode(file_get_contents($messageQueueFile), true) ?? []; $queue[] = [ 'target' => $clientAddr, 'message' => $message . "\n" ]; file_put_contents($messageQueueFile, json_encode($queue)); echo json_encode(['status' => 'success', 'msg' => 'Message queued for sending']); exit; } // GET请求返回在线客户端列表 $onlineUsers = json_decode(file_get_contents($onlineUsersFile), true) ?? []; echo json_encode(['online_clients' => $onlineUsers]); ?>
三、创建HTML管理页面(admin.html)
这个页面展示在线客户端列表,提供表单实现向指定客户端发送消息的功能:
<!DOCTYPE html> <html> <head> <title>TCP Socket Server Admin</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 20px auto; padding: 0 20px; } .client-section { margin: 20px 0; padding: 15px; border: 1px solid #eee; border-radius: 6px; } .client-item { padding: 8px; margin: 5px 0; background: #f8f9fa; border-radius: 4px; } #send-form { margin-top: 25px; } .form-group { margin: 12px 0; } label { display: block; margin-bottom: 6px; font-weight: 500; } select, textarea { width: 100%; padding: 8px; box-sizing: border-box; border: 1px solid #ddd; border-radius: 4px; } button { padding: 10px 20px; background: #0d6efd; color: #fff; border: none; border-radius: 4px; cursor: pointer; } button:hover { background: #0b5ed7; } .status { margin-top: 15px; padding: 10px; border-radius: 4px; display: none; } .status.success { display: block; background: #d1e7dd; color: #0f5132; } .status.error { display: block; background: #f8d7da; color: #842029; } </style> </head> <body> <h1>TCP Socket Server Admin Panel</h1> <div class="client-section"> <h2>Online Clients</h2> <div id="client-list"></div> </div> <form id="send-form"> <div class="form-group"> <label for="client-select">Target Client:</label> <select id="client-select" required></select> </div> <div class="form-group"> <label for="message-content">Message:</label> <textarea id="message-content" rows="3" placeholder="Enter your message..." required></textarea> </div> <button type="submit">Send Message</button> </form> <div id="status" class="status"></div> <script> // 刷新在线客户端列表 function loadClients() { fetch('send_message.php') .then(res => res.json()) .then(data => { const clientList = document.getElementById('client-list'); const clientSelect = document.getElementById('client-select'); clientList.innerHTML = ''; clientSelect.innerHTML = ''; if (data.online_clients.length === 0) { clientList.innerHTML = '<p>No clients currently online</p>'; clientSelect.innerHTML = '<option value="" disabled selected>No clients available</option>'; clientSelect.disabled = true; return; } clientSelect.disabled = false; data.online_clients.forEach(addr => { // 添加到展示列表 const item = document.createElement('div'); item.className = 'client-item'; item.textContent = addr; clientList.appendChild(item); // 添加到下拉选择框 const option = document.createElement('option'); option.value = addr; option.textContent = addr; clientSelect.appendChild(option); }); }) .catch(err => { showStatus('Failed to load client list: ' + err.message, 'error'); }); } // 显示状态提示 function showStatus(text, type) { const statusDiv = document.getElementById('status'); statusDiv.textContent = text; statusDiv.className = `status ${type}`; setTimeout(() => { statusDiv.className = 'status'; }, 3000); } // 处理表单提交 document.getElementById('send-form').addEventListener('submit', function(e) { e.preventDefault(); const clientAddr = document.getElementById('client-select').value; const message = document.getElementById('message-content').value; fetch('send_message.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: `client_addr=${encodeURIComponent(clientAddr)}&message=${encodeURIComponent(message)}` }) .then(res => res.json()) .then(data => { if (data.status === 'success') { showStatus(data.msg, 'success'); document.getElementById('message-content').value = ''; } else { showStatus(data.msg, 'error'); } }) .catch(err => { showStatus('Failed to send message: ' + err.message, 'error'); }); }); // 页面加载时初始化,每5秒刷新一次客户端列表 loadClients(); setInterval(loadClients, 5000); </script> </body> </html>
四、部署注意事项
- 确保
online_users.json和message_queue.json文件有写入权限(可通过FTP设置文件权限为755或777) - 启动TCP服务器:由于共享主机无控制台,可创建一个启动脚本(如
start_server.php),通过浏览器访问一次来启动服务器,注意主机商是否允许长期运行脚本 - 确认端口
8085在主机商处已开放,若无法使用该端口,可更换为主机允许的端口
内容的提问来源于stack exchange,提问作者MMahya RA
相关产品推荐
相关产品推荐

