Golang远程桌面唤醒工具 - 通过Wake-On-Lan实现桌面连接

Golang远程桌面唤醒工具 - 通过Wake-On-Lan实现桌面连接 如何用Go开发一个通过网络连接桌面电脑的Wake-On-Lan工具?

预期功能:

  • 基于Web的应用程序,无需登录和密码(我们将通过IP控制访问,无需基础设施的反向代理)
  • 桌面列表应用程序
  • 每个工作空间旁边显示其启用或禁用状态的信息
  • 用于开启桌面的按钮

详细信息:

  • 要列出桌面,我们可以使用一个配置文件,其中包含桌面名称和已配置WoL的MAC地址。
  • 要检查桌面是否已开启,我们可以研究通过MAC地址使用“ping”的方法(例如Linux中的工具:arping)。
  • 要连接桌面,我们可以使用发送WoL数据包的库,甚至调用我们手动使用的外部命令。
  • 我们将使用go modules,而不是gopath。

更多关于Golang远程桌面唤醒工具 - 通过Wake-On-Lan实现桌面连接的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang远程桌面唤醒工具 - 通过Wake-On-Lan实现桌面连接的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


以下是一个完整的Go语言实现,包含Web界面、配置文件解析、WoL唤醒和状态检测功能:

// main.go
package main

import (
	"encoding/json"
	"fmt"
	"html/template"
	"log"
	"net"
	"net/http"
	"os"
	"os/exec"
	"strings"
	"sync"
	"time"
)

// Desktop 结构体定义
type Desktop struct {
	Name string `json:"name"`
	MAC  string `json:"mac"`
	IP   string `json:"ip"`
}

// Config 配置文件结构
type Config struct {
	Desktops []Desktop `json:"desktops"`
	Port     int       `json:"port"`
}

// 全局变量
var (
	config     Config
	configFile = "config.json"
	tmpl       *template.Template
	mu         sync.RWMutex
)

func init() {
	// 加载模板
	tmpl = template.Must(template.ParseFiles("templates/index.html"))
}

func main() {
	// 加载配置
	loadConfig()

	// 设置路由
	http.HandleFunc("/", handleIndex)
	http.HandleFunc("/wake", handleWake)
	http.HandleFunc("/status", handleStatus)
	http.HandleFunc("/api/desktops", handleAPI)

	// 启动服务器
	addr := fmt.Sprintf(":%d", config.Port)
	log.Printf("服务器启动在 http://localhost%s", addr)
	log.Fatal(http.ListenAndServe(addr, nil))
}

// 加载配置文件
func loadConfig() {
	data, err := os.ReadFile(configFile)
	if err != nil {
		// 创建默认配置
		config = Config{
			Port: 8080,
			Desktops: []Desktop{
				{Name: "办公室电脑", MAC: "AA:BB:CC:DD:EE:FF", IP: "192.168.1.100"},
				{Name: "开发服务器", MAC: "11:22:33:44:55:66", IP: "192.168.1.101"},
			},
		}
		saveConfig()
		return
	}

	if err := json.Unmarshal(data, &config); err != nil {
		log.Fatalf("配置文件解析错误: %v", err)
	}
}

// 保存配置
func saveConfig() {
	data, err := json.MarshalIndent(config, "", "  ")
	if err != nil {
		log.Printf("配置序列化错误: %v", err)
		return
	}
	if err := os.WriteFile(configFile, data, 0644); err != nil {
		log.Printf("保存配置文件错误: %v", err)
	}
}

// 发送Wake-on-LAN魔术包
func sendWOL(mac string) error {
	// 解析MAC地址
	hwAddr, err := net.ParseMAC(mac)
	if err != nil {
		return fmt.Errorf("无效的MAC地址: %v", err)
	}

	// 创建魔术包: 6字节FF + 16次重复MAC地址
	packet := make([]byte, 102)
	for i := 0; i < 6; i++ {
		packet[i] = 0xFF
	}
	for i := 1; i <= 16; i++ {
		copy(packet[i*6:], hwAddr)
	}

	// 发送到广播地址
	conn, err := net.Dial("udp", "255.255.255.255:9")
	if err != nil {
		// 尝试使用本地广播地址
		conn, err = net.Dial("udp", "192.168.1.255:9")
		if err != nil {
			return fmt.Errorf("网络连接失败: %v", err)
		}
	}
	defer conn.Close()

	_, err = conn.Write(packet)
	return err
}

// 检查桌面状态(使用ping)
func checkStatus(ip string) (bool, error) {
	var cmd *exec.Cmd
	
	// 根据操作系统使用不同的ping命令
	if strings.Contains(strings.ToLower(os.Getenv("OS")), "windows") {
		cmd = exec.Command("ping", "-n", "1", "-w", "1000", ip)
	} else {
		cmd = exec.Command("ping", "-c", "1", "-W", "1", ip)
	}
	
	err := cmd.Run()
	return err == nil, nil
}

// 使用arping检查状态(Linux)
func checkStatusWithArping(mac, ip string) (bool, error) {
	cmd := exec.Command("arping", "-c", "1", "-w", "1", ip)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return false, nil
	}
	
	// 检查输出中是否包含MAC地址
	return strings.Contains(string(output), strings.ToUpper(mac)), nil
}

// 处理主页请求
func handleIndex(w http.ResponseWriter, r *http.Request) {
	mu.RLock()
	defer mu.RUnlock()
	
	data := struct {
		Desktops []Desktop
	}{
		Desktops: config.Desktops,
	}
	
	tmpl.Execute(w, data)
}

// 处理唤醒请求
func handleWake(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, "方法不允许", http.StatusMethodNotAllowed)
		return
	}
	
	mac := r.FormValue("mac")
	if mac == "" {
		http.Error(w, "缺少MAC地址", http.StatusBadRequest)
		return
	}
	
	// 发送WoL包
	if err := sendWOL(mac); err != nil {
		http.Error(w, fmt.Sprintf("唤醒失败: %v", err), http.StatusInternalServerError)
		return
	}
	
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("唤醒信号已发送"))
}

// 处理状态检查
func handleStatus(w http.ResponseWriter, r *http.Request) {
	mac := r.URL.Query().Get("mac")
	ip := r.URL.Query().Get("ip")
	
	if mac == "" || ip == "" {
		http.Error(w, "缺少参数", http.StatusBadRequest)
		return
	}
	
	var isOnline bool
	var err error
	
	// 尝试使用arping(Linux)
	if _, err := exec.LookPath("arping"); err == nil {
		isOnline, err = checkStatusWithArping(mac, ip)
	} else {
		// 回退到ping
		isOnline, err = checkStatus(ip)
	}
	
	if err != nil {
		http.Error(w, fmt.Sprintf("状态检查失败: %v", err), http.StatusInternalServerError)
		return
	}
	
	status := "offline"
	if isOnline {
		status = "online"
	}
	
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status": status,
		"mac":    mac,
		"ip":     ip,
	})
}

// API接口:获取所有桌面状态
func handleAPI(w http.ResponseWriter, r *http.Request) {
	mu.RLock()
	defer mu.RUnlock()
	
	type DesktopStatus struct {
		Desktop
		Status    string `json:"status"`
		Timestamp int64  `json:"timestamp"`
	}
	
	var results []DesktopStatus
	var wg sync.WaitGroup
	var muResult sync.Mutex
	
	for _, desktop := range config.Desktops {
		wg.Add(1)
		go func(d Desktop) {
			defer wg.Done()
			
			isOnline, _ := checkStatus(d.IP)
			status := "offline"
			if isOnline {
				status = "online"
			}
			
			muResult.Lock()
			results = append(results, DesktopStatus{
				Desktop:   d,
				Status:    status,
				Timestamp: time.Now().Unix(),
			})
			muResult.Unlock()
		}(desktop)
	}
	
	wg.Wait()
	
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(results)
}
<!-- templates/index.html -->
<!DOCTYPE html>
<html>
<head>
    <title>远程桌面唤醒工具</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            background: white;
            border-radius: 8px;
            padding: 20px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 {
            color: #333;
            border-bottom: 2px solid #4CAF50;
            padding-bottom: 10px;
        }
        .desktop-list {
            margin-top: 20px;
        }
        .desktop-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 15px;
            margin: 10px 0;
            background: #f9f9f9;
            border-radius: 5px;
            border-left: 4px solid #ddd;
        }
        .desktop-info {
            flex: 1;
        }
        .desktop-name {
            font-weight: bold;
            font-size: 16px;
            color: #333;
        }
        .desktop-details {
            font-size: 14px;
            color: #666;
            margin-top: 5px;
        }
        .status {
            padding: 4px 12px;
            border-radius: 12px;
            font-size: 12px;
            font-weight: bold;
            margin-right: 15px;
        }
        .status.online {
            background: #d4edda;
            color: #155724;
        }
        .status.offline {
            background: #f8d7da;
            color: #721c24;
        }
        .btn {
            padding: 8px 16px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
            transition: background 0.3s;
        }
        .btn-wake {
            background: #4CAF50;
            color: white;
        }
        .btn-wake:hover {
            background: #45a049;
        }
        .btn-wake:disabled {
            background: #cccccc;
            cursor: not-allowed;
        }
        .loading {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 3px solid #f3f3f3;
            border-top: 3px solid #4CAF50;
            border-radius: 50%;
            animation: spin 1s linear infinite;
            margin-right: 10px;
            vertical-align: middle;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>远程桌面唤醒工具</h1>
        
        <div class="desktop-list">
            {{range .Desktops}}
            <div class="desktop-item" id="desktop-{{.MAC}}">
                <div class="desktop-info">
                    <div class="desktop-name">{{.Name}}</div>
                    <div class="desktop-details">
                        MAC: {{.MAC}} | IP: {{.IP}}
                    </div>
                </div>
                <div>
                    <span class="status offline" id="status-{{.MAC}}">离线</span>
                    <button class="btn btn-wake" onclick="wakeDesktop('{{.MAC}}')" id="btn-{{.MAC}}">
                        唤醒
                    </button>
                </div>
            </div>
            {{end}}
        </div>
    </div>

    <script>
        // 更新桌面状态
        async function updateStatus(mac, ip) {
            const statusElem = document.getElementById(`status-${mac}`);
            const btnElem = document.getElementById(`btn-${mac}`);
            
            statusElem.textContent = '检查中...';
            statusElem.className = 'status';
            btnElem.disabled = true;
            
            try {
                const response = await fetch(`/status?mac=${encodeURIComponent(mac)}&ip=${encodeURIComponent(ip)}`);
                const data = await response.json();
                
                if (data.status === 'online') {
                    statusElem.textContent = '在线';
                    statusElem.className = 'status online';
                    btnElem.disabled = true;
                } else {
                    statusElem.textContent = '离线';
                    statusElem.className = 'status offline';
                    btnElem.disabled = false;
                }
            } catch (error) {
                console.error('状态检查失败:', error);
                statusElem.textContent = '错误';
                btnElem.disabled = false;
            }
        }
        
        // 唤醒桌面
        async function wakeDesktop(mac) {
            const btnElem = document.getElementById(`btn-${mac}`);
            const originalText = btnElem.textContent;
            
            btnElem.textContent = '唤醒中...';
            btnElem.disabled = true;
            
            try {
                const formData = new FormData();
                formData.append('mac', mac);
                
                const response = await fetch('/wake', {
                    method: 'POST',
                    body: formData
                });
                
                if (response.ok) {
                    alert('唤醒信号已发送');
                    // 等待一段时间后重新检查状态
                    setTimeout(() => {
                        const desktop = getDesktopByMAC(mac);
                        if (desktop) {
                            updateStatus(mac, desktop.ip);
                        }
                    }, 5000);
                } else {
                    alert('唤醒失败');
                }
            } catch (error) {
                console.error('唤醒失败:', error);
                alert('网络错误');
            } finally {
                btnElem.textContent = originalText;
                btnElem.disabled = false;
            }
        }
        
        // 获取桌面信息(简化实现)
        function getDesktopByMAC(mac) {
            // 这里应该从服务器获取或从页面数据中提取
            // 简化实现:假设IP已知或从页面元素获取
            const elem = document.getElementById(`desktop-${mac}`);
            if (elem) {
                const ipMatch = elem.textContent.match(/IP:\s*([\d\.]+)/);
                return {
                    mac: mac,
                    ip: ipMatch ? ipMatch[1] : ''
                };
            }
            return null;
        }
        
        // 页面加载时初始化状态检查
        document.addEventListener('DOMContentLoaded', function() {
            // 初始检查所有桌面状态
            {{range .Desktops}}
            setTimeout(() => updateStatus('{{.MAC}}', '{{.IP}}'), 100);
            {{end}}
            
            // 每30秒自动更新状态
            setInterval(() => {
                {{range .Desktops}}
                updateStatus('{{.MAC}}', '{{.IP}}');
                {{end}}
            }, 30000);
        });
    </script>
</body>
</html>
// config.json 配置文件示例
{
  "port": 8080,
  "desktops": [
    {
      "name": "办公室电脑",
      "mac": "AA:BB:CC:DD:EE:FF",
      "ip": "192.168.1.100"
    },
    {
      "name": "开发服务器",
      "mac": "11:22:33:44:55:66",
      "ip": "192.168.1.101"
    }
  ]
}
// go.mod
module wol-tool

go 1.21

require (
    golang.org/x/net v0.17.0
)

使用说明:

  1. 安装依赖
go mod init wol-tool
go mod tidy
  1. 创建目录结构
project/
├── main.go
├── go.mod
├── config.json
└── templates/
    └── index.html
  1. 配置桌面: 编辑config.json文件,添加你的桌面MAC地址和IP

  2. 运行程序

go run main.go
  1. 访问Web界面: 打开浏览器访问 http://localhost:8080

功能特点:

  1. 纯Go实现:不依赖外部命令(WoL包发送)
  2. 双状态检测:优先使用arping,回退到ping
  3. 实时状态更新:前端每30秒自动刷新
  4. 响应式设计:简洁的Web界面
  5. 并发处理:同时检查多个桌面状态
  6. 配置驱动:JSON配置文件管理桌面列表

注意事项:

  1. 确保网络设备支持Wake-on-LAN
  2. 需要在BIOS中启用WoL功能
  3. 网络交换机可能需要配置相关设置
  4. 防火墙需要允许UDP端口9的广播流量
  5. 对于跨子网唤醒,需要配置路由器转发WoL包

这个实现完全符合你的需求,无需登录认证(依赖IP访问控制),提供了完整的桌面列表、状态显示和唤醒功能。

回到顶部