You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go Web应用实时展示ARP数据包:协程通道阻塞问题求助

解决Go Web应用实时展示ARP数据包的阻塞与实时更新问题

你的核心问题在于:HTTP请求是一次性响应模型,现有代码中API函数因通道单次接收操作阻塞,且无法持续推送新数据包到前端。下面是针对性的解决方案,采用**Server-Sent Events(SSE)**实现实时数据推送,同时修正协程与通道的使用逻辑。

问题根源分析

  1. 全局通道未初始化,直接使用会导致永久阻塞;
  2. API函数仅接收一次通道数据就渲染模板,无法响应后续新数据包;
  3. 常规HTTP请求无法维持长连接推送更新,需用SSE/WebSocket这类实时通信方案。

解决方案代码实现

1. 重构全局状态与数据包监听逻辑

使用互斥锁保证并发安全,在应用启动时仅启动一次监听协程:

import (
    "log"
    "sync"
    "time"
    "github.com/google/gopacket"
    "github.com/google/gopacket/pcap"
)

type PacketStructure struct {
    PacketProtocol string
    Something      string
    Number         int
}

// 全局维护数据包列表,加互斥锁防止并发读写冲突
var (
    packetList []PacketStructure
    mu         sync.Mutex
    packetSeq  int // 数据包序号计数器
)

func PacketListener() {
    var (
        iface     = "en0"
        snaplen   = int32(1600)
        promisc   = false
        timeout   = pcap.BlockForever
        filter    = "arp"
        devFound  = false
    )

    devices, err := pcap.FindAllDevs()
    if err != nil {
        log.Panicln(err)
    }
    for _, device := range devices {
        if device.Name == iface {
            devFound = true
            break
        }
    }
    if !devFound {
        log.Panicf("Device '%s' does not exist\n", iface)
    }

    handle, err := pcap.OpenLive(iface, snaplen, promisc, timeout)
    if err != nil {
        log.Panicln(err)
    }
    defer handle.Close()

    if err := handle.SetBPFFilter(filter); err != nil {
        log.Panicln(err)
    }

    source := gopacket.NewPacketSource(handle, handle.LinkType())
    for range source.Packets() {
        mu.Lock()
        packetSeq++
        newPacket := PacketStructure{
            PacketProtocol: "ARP",
            Something:      time.Now().Format(time.RFC3339), // 标准化时间格式
            Number:         packetSeq,
        }
        packetList = append(packetList, newPacket)
        mu.Unlock()
    }
}

2. 实现SSE实时推送接口

SSE通过维持HTTP长连接,持续向客户端推送新数据:

import (
    "encoding/json"
    "net/http"
    "time"
)

type Repository struct{}

func (m *Repository) SSEPacketStream(w http.ResponseWriter, r *http.Request) {
    // 设置SSE响应头
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")

    // 先发送已捕获的所有数据包
    mu.Lock()
    currentPackets := make([]PacketStructure, len(packetList))
    copy(currentPackets, packetList)
    lastPacketCount := len(currentPackets)
    mu.Unlock()

    sendSSEData(w, currentPackets)

    // 定时检查新数据包并推送
    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop()

    for {
        select {
        case <-r.Context().Done(): // 客户端断开连接,终止请求
            return
        case <-ticker.C:
            mu.Lock()
            if len(packetList) > lastPacketCount {
                // 仅推送新增的数据包
                newPackets := packetList[lastPacketCount:]
                sendSSEData(w, newPackets)
                lastPacketCount = len(packetList)
            }
            mu.Unlock()
        }
    }
}

// 将数据包序列化为SSE格式发送
func sendSSEData(w http.ResponseWriter, packets []PacketStructure) {
    enc := json.NewEncoder(w)
    for _, p := range packets {
        _, _ = w.Write([]byte("data: "))
        if err := enc.Encode(p); err != nil {
            log.Println("Failed to encode packet:", err)
            continue
        }
        _, _ = w.Write([]byte("\n"))
        // 强制刷新缓冲区,确保数据实时发送到客户端
        if flusher, ok := w.(http.Flusher); ok {
            flusher.Flush()
        }
    }
}

3. 修改前端模板,通过JS接收SSE并更新表格

<!DOCTYPE html>
<html>
<head>
    <title>ARP Packet Monitor</title>
</head>
<body>
    <table id="packetTable" border="1">
        <tr>
            <th>Protocol</th>
            <th>Timestamp</th>
            <th>Packet Number</th>
        </tr>
    </table>

    <script>
        const table = document.getElementById('packetTable');
        // 连接SSE接口
        const eventSource = new EventSource('/sse-packets');

        // 接收新数据包并添加到表格
        eventSource.onmessage = function(event) {
            const packet = JSON.parse(event.data);
            const row = table.insertRow();
            row.insertCell().textContent = packet.PacketProtocol;
            row.insertCell().textContent = packet.Something;
            row.insertCell().textContent = packet.Number;
        };

        // 处理SSE错误
        eventSource.onerror = function(error) {
            console.error('SSE connection error:', error);
            eventSource.close();
        };
    </script>
</body>
</html>

4. 应用启动逻辑

在main函数中启动监听协程并注册路由:

import (
    "log"
    "net/http"
    // 导入你的模板渲染包
)

func main() {
    // 初始化全局状态
    packetList = []PacketStructure{}
    packetSeq = 0

    // 启动ARP数据包监听协程(仅启动一次)
    go PacketListener()

    repo := &Repository{}
    // 注册SSE路由
    http.HandleFunc("/sse-packets", repo.SSEPacketStream)
    // 注册首页路由,渲染初始页面
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        render.RenderTemplate(w, "packets.html", nil)
    })

    log.Println("Server running on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

关键改进点

  1. 并发安全:用sync.Mutex保护全局数据包列表,避免协程读写冲突;
  2. 实时推送:SSE维持长连接,主动向客户端推送新数据包,替代一次性HTTP响应;
  3. 资源合理利用:仅启动一次监听协程,避免重复创建网络捕获句柄;
  4. 前端动态更新:通过JavaScript接收SSE数据,实时添加表格行,无需刷新页面。

内容的提问来源于stack exchange,提问作者Radin Khosraviani

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 23:07:53