基于Go语言实现一个简易远程传屏工具

Written by

in

文章目录
  • 这个项目的架构简单得不能再简单了:一个服务端,一个客户端。服务端负责截图并发送,客户端负责接收并显示。就像两个人打电话,一个说,一个听,完美配合! 让我们先看看这个项目的目录结构: 远程传屏/ ├── client/ │ ├── main.go # 客户端代码 │ └── go.mod # 客户端依赖 └── server/ ├── main.go # 服务端代码 └── go.mod # 服务端依赖
  • 服务端的主要工作就是不断地「咔嚓咔嚓」截图,然后把图片发送给客户端。让我们看看服务端的核心代码: func main() { Loger, _ = mgxlog.NewMgxLog(“c:/runlog/”, 10*1024*1024, 100, 3, 1000) port := 1211 listener, err := net.Listen(“tcp”, “:”+strconv.Itoa(port)) if err != nil { Loger.Errorf(“Failed to listen on port: “, err) } Loger.Infof(“Listening on port %d, waiting for image data…n”, port) // 循环接受连接 for { conn, err := listener.Accept() if err != nil { Loger.Infof(“Error accepting connection: “, err) continue } go handleConnection(conn) } } 服务端首先启动一个TCP监听,然后等待客户端连接。注意这里用了goroutine来处理每个连接,这样就可以同时服务多个客户端了! 接下来是身份验证部分: func handleConnection(conn net.Conn) { defer conn.Close() for { reader := bufio.NewReader(conn) info, err := reader.ReadString(‘n’) if err != nil { Loger.Infof(“read user info err: “, err) return } if info != “administrator:mgx780707mgxn” { Loger.Infof(“user info err: “, info) return } Loger.Infof(“user login ok:”, info) captureScreenshots(conn) } } 这里有个简单的身份验证机制,客户端必须发送正确的用户名和密码才能继续。不过说实话,这个密码直接硬编码在代码里,安全性嘛… 咱们就当是内部工具,别太较真~ 最核心的截图功能在这里: func Capture() (int, int, []byte, error) { width := int(win.GetSystemMetrics(win.SM_CXSCREEN)) height := int(win.GetSystemMetrics(win.SM_CYSCREEN)) // … [Windows API截图代码] … var buf bytes.Buffer err = png.Encode(&buf, img) if err != nil { return width, height, nil, err } return width, height, buf.Bytes(), nil } 服务端使用Windows API来捕获整个屏幕,然后将截图编码为PNG格式。这里还有个小优化: // 计算每片数据的大小 count := (len(datas) + 999) / 1000 chunkSize := (len(datas) + count – 1) / count // … [分片发送代码] if ld, ok := lastdatas[i]; ok { //有老的对比 if bytes.Equal(ld, data) { data = []byte{} } } 看到了吗?代码会记录上一次发送的数据,如果当前分片和上次一样,就发送一个空数据块。这样可以节省带宽,特别是当屏幕大部分内容没有变化的时候!
  • 客户端的工作相对简单一些:连接服务器,接收图片数据,然后显示出来。让我们看看客户端的GUI部分: func main() { gtk.Init(nil) window, _ := gtk.WindowNew(gtk.WINDOW_TOPLEVEL) window.SetDefaultSize(800, 600) window.Connect(“destroy”, func() { gtk.MainQuit() }) box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) window.Add(box) image, _ := gtk.ImageNew() box.PackStart(image, true, true, 0) go updateImageAsync(image) // 异步更新图像 window.ShowAll() gtk.Main() } 客户端使用GTK库创建了一个简单的窗口,里面放了一个图像控件。然后启动一个goroutine来异步接收和更新图像,这样就不会阻塞UI线程了。 接收数据的逻辑在这里: func updateImageAsync(image *gtk.Image) { // 服务器地址和端口 serverAddr := “192.168.2.26:1211” // 连接到服务器 conn, err := net.Dial(“tcp”, serverAddr) if err != nil { log.Printf(“Failed to connect to server: %vn”, err) os.Exit(1) } defer conn.Close() conn.Write([]byte(“administrator:mgx780707mgxn”)) // … [数据接收和处理代码] … } 客户端首先连接到服务器,发送用户名密码,然后开始接收数据。当接收到完整的图像数据后,就更新UI显示: glib.IdleAdd(func() { loader, _ := gdk.PixbufLoaderNew() loader.Write(pngdata) loader.Close() pixbuf, _ := loader.GetPixbuf() image.SetSizeRequest(int(ifi.Width), int(ifi.Height)) // 将图像加载到图像控件 image.SetFromPixbuf(pixbuf) image.QueueDraw() fmt.Printf(“Updated image: width=%d, height=%dn”, ifi.Width, ifi.Height) }) 这里使用了glib.IdleAdd来确保在GTK的主事件循环中更新UI,这是GUI编程的常见做法。
  • 这个项目定义了一个简单的数据结构来传输图像信息: type ImgFpInfo struct { Dsize uint32 // 数据大小 Type uint8 // 数据类型 Width uint32 // 图像宽度 Height uint32 // 图像高度 Dq uint16 // 当前数据块序号 Zs uint16 // 总数据块数量 Datas []byte // 实际图像数据 } 这个结构包含了图像的基本信息,以及数据分块的信息。服务端将图像分成多个小块发送,客户端接收后再重新组装起来。
  • 这个远程传屏工具虽然简单,但功能完整,而且有一些不错的优化: 使用TCP保证数据传输的可靠性 增量更新,只发送变化的部分 数据分块传输,避免大文件传输问题 异步处理,保证UI流畅 当然,这个工具还有很多可以改进的地方,比如: 更安全的身份验证机制 加密传输数据 支持多屏幕选择 增加控制功能(比如远程操作) 不过,作为一个简单的远程传屏工具,它已经能够满足基本需求了。如果你有兴趣,可以基于这个代码进行扩展和改进! 最后,附上项目中使用的自定义数据结构和工具函数,方便大家理解整个数据流: // 服务器端的工具方法 func (ifi *ImgFpInfo) GetBytes() []byte { b := bytes.NewBuffer([]byte{}) binary.Write(b, binary.BigEndian, ifi.Dsize) binary.Write(b, binary.BigEndian, ifi.Type) binary.Write(b, binary.BigEndian, ifi.Width) binary.Write(b, binary.BigEndian, ifi.Height) binary.Write(b, binary.BigEndian, ifi.Dq) binary.Write(b, binary.BigEndian, ifi.Zs) b.Write(ifi.Datas) return b.Bytes() } // 客户端的数据解析方法 func dataToImgFpInfo(data []byte) ImgFpInfo { ifi := ImgFpInfo{} ifi.Type = uint8(data[0]) binary.Read(bytes.NewBuffer(data[1:5]), binary.BigEndian, &ifi.Width) binary.Read(bytes.NewBuffer(data[5:9]), binary.BigEndian, &ifi.Height) binary.Read(bytes.NewBuffer(data[9:11]), binary.BigEndian, &ifi.Dq) binary.Read(bytes.NewBuffer(data[11:13]), binary.BigEndian, &ifi.Zs) ifi.Datas = data[13:] if len(ifi.Datas) == 0 { ifi.Datas = lastdatas[ifi.Dq] } return ifi } 完整源码如下: server.go package main import ( “bufio” “bytes” “encoding/binary” “errors” “image” “image/png” “net” “strconv” “time” “unsafe” win “github.com/lxn/win” “gitcode.com/jjgtmgx/mgxlog” ) var Loger *mgxlog.MgxLog func main() { Loger, _ = mgxlog.NewMgxLog(“c:/runlog/”, 10*1024*1024, 100, 3, 1000) port := 1211 listener, err := net.Listen(“tcp”, “:”+strconv.Itoa(port)) if err != nil { Loger.Errorf(“Failed to listen on port: “, err) } Loger.Infof(“Listening on port %d, waiting for image data…n”, port) // Receive and display the image for { conn, err := listener.Accept() if err != nil { Loger.Infof(“Error accepting connection: “, err) continue } go handleConnection(conn) } } func handleConnection(conn net.Conn) { defer conn.Close() for { reader := bufio.NewReader(conn) // Read the length of the image data info, err := reader.ReadString(‘n’) if err != nil { Loger.Infof(“read user info err: “, err) return } if info != “administrator:mgx780707mgxn” { Loger.Infof(“user info err: “, info) return } Loger.Infof(“user login ok:”, info) captureScreenshots(conn) } } func captureScreenshots(connection net.Conn) error { for { w, h, datas, err := Capture() if err != nil { Loger.Infof(“capture err: %vn”, err) return err } err = sendBitmapData(connection, w, h, datas) if err != nil { Loger.Infof(“send png err: %vn”, err) return err } // 休眠一段时间以控制截图频率 time.Sleep(41 * time.Millisecond) } } var lastdatas = make(map[int][]byte) func sendBitmapData(conn net.Conn, w, h int, datas []byte) error { // 计算每片数据的大小 count := (len(datas) + 999) / 1000 chunkSize := (len(datas) + count – 1) / count // 将位图的宽度和高度转换为最节约的数据类型 width := uint32(w) height := uint32(h) // 分片发送位图数据 for i := 0; i < count; i++ { // 计算当前片的起始位置和大小 start := i * chunkSize end := start + chunkSize if end > len(datas) { end = len(datas) } // 获取当前片的数据 data := datas[start:end] if ld, ok := lastdatas[i]; ok { //有老的对比 if bytes.Equal(ld, data) { data = []byte{} } } if len(data) > 0 { lastdatas[i] = data } dataLen := int32(len(data)) totalsize := uint32(17 + dataLen) ifi := ImgFpInfo{ Dsize: totalsize, Type: 1, Width: width, Height: height, Dq: uint16(i), Zs: uint16(count), Datas: data, } // 发送当前片的数据 bs := ifi.GetBytes() if _, err := conn.Write(bs); err != nil { return err } //fmt.Println(ifi) } return nil } func Capture() (int, int, []byte, error) { width := int(win.GetSystemMetrics(win.SM_CXSCREEN)) height := int(win.GetSystemMetrics(win.SM_CYSCREEN)) rect := image.Rect(0, 0, width, height) img, err := CreateImage(rect) if err != nil { return width, height, nil, err } //hwnd := win.GetDesktopWindow() hdc := win.GetDC(0) if hdc == 0 { return width, height, nil, errors.New(“GetDC failed”) } defer win.ReleaseDC(0, hdc) memory_device := win.CreateCompatibleDC(hdc) if memory_device == 0 { return width, height, nil, errors.New(“CreateCompatibleDC failed”) } defer win.DeleteDC(memory_device) bitmap := win.CreateCompatibleBitmap(hdc, int32(width), int32(height)) if bitmap == 0 { return width, height, nil, errors.New(“CreateCompatibleBitmap failed”) } defer win.DeleteObject(win.HGDIOBJ(bitmap)) var header win.BITMAPINFOHEADER header.BiSize = uint32(unsafe.Sizeof(header)) header.BiPlanes = 1 header.BiBitCount = 32 header.BiWidth = int32(width) header.BiHeight = int32(-height) header.BiCompression = win.BI_RGB header.BiSizeImage = 0 bitmapDataSize := uintptr(((int64(width)*int64(header.BiBitCount) + 31) / 32) * 4 * int64(height)) hmem := win.GlobalAlloc(win.GMEM_MOVEABLE, bitmapDataSize) defer win.GlobalFree(hmem) memptr := win.GlobalLock(hmem) defer win.GlobalUnlock(hmem) old := win.SelectObject(memory_device, win.HGDIOBJ(bitmap)) if old == 0 { return width, height, nil, errors.New(“SelectObject failed”) } defer win.SelectObject(memory_device, old) if !win.BitBlt(memory_device, 0, 0, int32(width), int32(height), hdc, int32(0), int32(0), win.SRCCOPY) { return width, height, nil, errors.New(“BitBlt failed”) } if win.GetDIBits(hdc, bitmap, 0, uint32(height), (*uint8)(memptr), (*win.BITMAPINFO)(unsafe.Pointer(&header)), win.DIB_RGB_COLORS) == 0 { return width, height, nil, errors.New(“GetDIBits failed”) } i := 0 src := uintptr(memptr) for y := 0; y < height; y++ { for x := 0; x < width; x++ { v0 := *(*uint8)(unsafe.Pointer(src)) v1 := *(*uint8)(unsafe.Pointer(src + 1)) v2 := *(*uint8)(unsafe.Pointer(src + 2)) img.Pix[i], img.Pix[i+1], img.Pix[i+2], img.Pix[i+3] = v2, v1, v0, 255 i += 4 src += 4 } } var buf bytes.Buffer err = png.Encode(&buf, img) if err != nil { return width, height, nil, err } return width, height, buf.Bytes(), nil } func CreateImage(rect image.Rectangle) (img *image.RGBA, e error) { img = nil e = errors.New(“Cannot create image.RGBA”) defer func() { err := recover() if err == nil { e = nil } }() img = image.NewRGBA(rect) return img, e } type ImgFpInfo struct { Dsize uint32 Type uint8 Width uint32 Height uint32 Dq uint16 Zs uint16 Datas []byte } func (ifi *ImgFpInfo) GetBytes() []byte { b := bytes.NewBuffer([]byte{}) binary.Write(b, binary.BigEndian, ifi.Dsize) binary.Write(b, binary.BigEndian, ifi.Type) binary.Write(b, binary.BigEndian, ifi.Width) binary.Write(b, binary.BigEndian, ifi.Height) binary.Write(b, binary.BigEndian, ifi.Dq) binary.Write(b, binary.BigEndian, ifi.Zs) b.Write(ifi.Datas) return b.Bytes() } client.go package main import ( “bytes” “encoding/binary” “fmt” “log” “net” “os” “github.com/gotk3/gotk3/gdk” “github.com/gotk3/gotk3/glib” “github.com/gotk3/gotk3/gtk” ) var ( width = 640 height = 480 ) func main() { gtk.Init(nil) window, _ := gtk.WindowNew(gtk.WINDOW_TOPLEVEL) window.SetDefaultSize(800, 600) window.Connect(“destroy”, func() { gtk.MainQuit() }) box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) window.Add(box) image, _ := gtk.ImageNew() box.PackStart(image, true, true, 0) go updateImageAsync(image) // 异步更新图像 window.ShowAll() gtk.Main() } // 异步更新图片 func updateImageAsync(image *gtk.Image) { // 服务器地址和端口 serverAddr := “192.168.2.26:1211” // 连接到服务器 conn, err := net.Dial(“tcp”, serverAddr) if err != nil { log.Printf(“Failed to connect to server: %vn”, err) os.Exit(1) } defer conn.Close() conn.Write([]byte(“administrator:mgx780707mgxn”)) BYTES_SIZE := 2048 HEAD_SIZE := 4 var ( buffer = bytes.NewBuffer(make([]byte, 0, BYTES_SIZE)) bytes = make([]byte, BYTES_SIZE) isHead bool = true contentSize int head = make([]byte, HEAD_SIZE) content = make([]byte, BYTES_SIZE) ) for { readLen, err := conn.Read(bytes) if err != nil { log.Printf(“read: %vn”, err) return } _, err = buffer.Write(bytes[0:readLen]) if err != nil { log.Printf(“read: %vn”, err) return } for { if isHead { if buffer.Len() >= HEAD_SIZE { _, err := buffer.Read(head) if err != nil { log.Printf(“read: %vn”, err) return } contentSize = int(binary.BigEndian.Uint32(head)) – HEAD_SIZE isHead = false } else { break } } if !isHead { if buffer.Len() >= contentSize { _, err := buffer.Read(content[:contentSize]) if err != nil { log.Printf(“read: %vn”, err) return } data := make([]byte, contentSize) copy(data, content) routeMessage(data, image) isHead = true } else { break } } } } } func routeMessage(data []byte, image *gtk.Image) { // 从socket读取PNG数据 ifi := dataToImgFpInfo(data) //fmt.Println(“read data:”, ifi) datas[ifi.Dq] = ifi.Datas okhash[ifi.Dq] = true if isok(int(ifi.Zs)) { pngdata := make([]byte, 0) for i := uint16(0); i < ifi.Zs; i++ { pngdata = append(pngdata, datas[i]…) } lastdatas = datas datas = make(map[uint16][]byte) okhash = make(map[uint16]bool) // 异步更新图像 glib.IdleAdd(func() { loader, _ := gdk.PixbufLoaderNew() loader.Write(pngdata) loader.Close() pixbuf, _ := loader.GetPixbuf() image.SetSizeRequest(int(ifi.Width), int(ifi.Height)) // 将图像加载到图像控件 image.SetFromPixbuf(pixbuf) image.QueueDraw() fmt.Printf(“Updated image: width=%d, height=%dn”, ifi.Width, ifi.Height) }) } } var lastdatas = make(map[uint16][]byte) var datas = make(map[uint16][]byte) var okhash = make(map[uint16]bool) func isok(t int) bool { if len(okhash) != t { return false } return true } func dataToImgFpInfo(data []byte) ImgFpInfo { ifi := ImgFpInfo{} ifi.Type = uint8(data[0]) binary.Read(bytes.NewBuffer(data[1:5]), binary.BigEndian, &ifi.Width) binary.Read(bytes.NewBuffer(data[5:9]), binary.BigEndian, &ifi.Height) binary.Read(bytes.NewBuffer(data[9:11]), binary.BigEndian, &ifi.Dq) binary.Read(bytes.NewBuffer(data[11:13]), binary.BigEndian, &ifi.Zs) ifi.Datas = data[13:] if len(ifi.Datas) == 0 { ifi.Datas = lastdatas[ifi.Dq] } return ifi } type ImgFpInfo struct { Dsize uint32 Type uint8 Width uint32 Height uint32 Dq uint16 Zs uint16 Datas []byte } 希望这篇文章能帮助你理解这个简单的远程传屏工具的实现原理。如果你有任何问题或者改进建议,欢迎在评论区留言! 以上就是基于Go语言实现一个简易远程传屏工具的详细内容,更多关于Go远程传屏工具的资料请关注风君子博客其它相关文章! 您可能感兴趣的文章: Goland使用delve进行远程调试的详细教程 Goland远程连接Linux进行项目开发的实现 Go中RPC远程过程调用的实现 详解Golang如何优雅接入多个远程配置中心 goland远程调试k8s上容器的实现
  • 目录
    • 项目架构:简单到「令人发指」
    • 服务端:「咔嚓」一声,屏幕被我「抓」住了
    • 客户端:「看,屏幕飞过来了!」
    • 数据传输协议:简单但有效
    • 小结:简单实用的小工具

    这个项目的架构简单得不能再简单了:一个服务端,一个客户端。服务端负责截图并发送,客户端负责接收并显示。就像两个人打电话,一个说,一个听,完美配合!

    让我们先看看这个项目的目录结构:

    远程传屏/
    ├── client/
    │   ├── main.go    # 客户端代码
    │   └── go.mod     # 客户端依赖
    └── server/
        ├── main.go    # 服务端代码
        └── go.mod     # 服务端依赖
    

    服务端的主要工作就是不断地「咔嚓咔嚓」截图,然后把图片发送给客户端。让我们看看服务端的核心代码:

    func main() {
        Loger, _ = mgxlog.NewMgxLog("c:/runlog/", 10*1024*1024, 100, 3, 1000)
        port := 1211
        listener, err := net.Listen("tcp", ":"+strconv.Itoa(port))
        if err != nil {
            Loger.Errorf("Failed to listen on port: ", err)
        }
        Loger.Infof("Listening on port %d, waiting for image data...n", port)
    
        // 循环接受连接
        for {
            conn, err := listener.Accept()
            if err != nil {
                Loger.Infof("Error accepting connection: ", err)
                continue
            }
            go handleConnection(conn)
        }
    }
    

    服务端首先启动一个TCP监听,然后等待客户端连接。注意这里用了goroutine来处理每个连接,这样就可以同时服务多个客户端了!

    接下来是身份验证部分:

    func handleConnection(conn net.Conn) {
        defer conn.Close()
        for {
            reader := bufio.NewReader(conn)
            info, err := reader.ReadString('n')
            if err != nil {
                Loger.Infof("read user info err: ", err)
                return
            }
            if info != "administrator:mgx780707mgxn" {
                Loger.Infof("user info err: ", info)
                return
            }
            Loger.Infof("user login ok:", info)
            captureScreenshots(conn)
        }
    }
    

    这里有个简单的身份验证机制,客户端必须发送正确的用户名和密码才能继续。不过说实话,这个密码直接硬编码在代码里,安全性嘛… 咱们就当是内部工具,别太较真~

    最核心的截图功能在这里:

    func Capture() (int, int, []byte, error) {
        width := int(win.GetSystemMetrics(win.SM_CXSCREEN))
        height := int(win.GetSystemMetrics(win.SM_CYSCREEN))
        // ... [Windows API截图代码] ...
        var buf bytes.Buffer
        err = png.Encode(&buf, img)
        if err != nil {
            return width, height, nil, err
        }
        return width, height, buf.Bytes(), nil
    }
    

    服务端使用Windows API来捕获整个屏幕,然后将截图编码为PNG格式。这里还有个小优化:

    // 计算每片数据的大小
    count := (len(datas) + 999) / 1000
    chunkSize := (len(datas) + count - 1) / count
    
    // ... [分片发送代码]
    if ld, ok := lastdatas[i]; ok { //有老的对比
        if bytes.Equal(ld, data) {
            data = []byte{}
        }
    }
    

    看到了吗?代码会记录上一次发送的数据,如果当前分片和上次一样,就发送一个空数据块。这样可以节省带宽,特别是当屏幕大部分内容没有变化的时候!

    客户端的工作相对简单一些:连接服务器,接收图片数据,然后显示出来。让我们看看客户端的GUI部分:

    func main() {
        gtk.Init(nil)
    
        window, _ := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
        window.SetDefaultSize(800, 600)
        window.Connect("destroy", func() {
            gtk.MainQuit()
        })
    
        box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
        window.Add(box)
    
        image, _ := gtk.ImageNew()
        box.PackStart(image, true, true, 0)
    
        go updateImageAsync(image) // 异步更新图像
    
        window.ShowAll()
        gtk.Main()
    }
    

    客户端使用GTK库创建了一个简单的窗口,里面放了一个图像控件。然后启动一个goroutine来异步接收和更新图像,这样就不会阻塞UI线程了。

    接收数据的逻辑在这里:

    func updateImageAsync(image *gtk.Image) {
        // 服务器地址和端口
        serverAddr := "192.168.2.26:1211"
    
        // 连接到服务器
        conn, err := net.Dial("tcp", serverAddr)
        if err != nil {
            log.Printf("Failed to connect to server: %vn", err)
            os.Exit(1)
        }
        defer conn.Close()
        conn.Write([]byte("administrator:mgx780707mgxn"))
        // ... [数据接收和处理代码] ...
    }
    

    客户端首先连接到服务器,发送用户名密码,然后开始接收数据。当接收到完整的图像数据后,就更新UI显示:

    glib.IdleAdd(func() {
        loader, _ := gdk.PixbufLoaderNew()
        loader.Write(pngdata)
        loader.Close()
        pixbuf, _ := loader.GetPixbuf()
        image.SetSizeRequest(int(ifi.Width), int(ifi.Height))
        // 将图像加载到图像控件
        image.SetFromPixbuf(pixbuf)
        image.QueueDraw()
        fmt.Printf("Updated image: width=%d, height=%dn", ifi.Width, ifi.Height)
    })
    

    这里使用了glib.IdleAdd来确保在GTK的主事件循环中更新UI,这是GUI编程的常见做法。

    这个项目定义了一个简单的数据结构来传输图像信息:

    type ImgFpInfo struct {
        Dsize  uint32  // 数据大小
        Type   uint8   // 数据类型
        Width  uint32  // 图像宽度
        Height uint32  // 图像高度
        Dq     uint16  // 当前数据块序号
        Zs     uint16  // 总数据块数量
        Datas  []byte  // 实际图像数据
    }
    

    这个结构包含了图像的基本信息,以及数据分块的信息。服务端将图像分成多个小块发送,客户端接收后再重新组装起来。

    这个远程传屏工具虽然简单,但功能完整,而且有一些不错的优化:

    1. 使用TCP保证数据传输的可靠性
    2. 增量更新,只发送变化的部分
    3. 数据分块传输,避免大文件传输问题
    4. 异步处理,保证UI流畅

    当然,这个工具还有很多可以改进的地方,比如:

    • 更安全的身份验证机制
    • 加密传输数据
    • 支持多屏幕选择
    • 增加控制功能(比如远程操作)

    不过,作为一个简单的远程传屏工具,它已经能够满足基本需求了。如果你有兴趣,可以基于这个代码进行扩展和改进!

    最后,附上项目中使用的自定义数据结构和工具函数,方便大家理解整个数据流:

    // 服务器端的工具方法
    func (ifi *ImgFpInfo) GetBytes() []byte {
        b := bytes.NewBuffer([]byte{})
        binary.Write(b, binary.BigEndian, ifi.Dsize)
        binary.Write(b, binary.BigEndian, ifi.Type)
        binary.Write(b, binary.BigEndian, ifi.Width)
        binary.Write(b, binary.BigEndian, ifi.Height)
        binary.Write(b, binary.BigEndian, ifi.Dq)
        binary.Write(b, binary.BigEndian, ifi.Zs)
        b.Write(ifi.Datas)
        return b.Bytes()
    }
    
    // 客户端的数据解析方法
    func dataToImgFpInfo(data []byte) ImgFpInfo {
        ifi := ImgFpInfo{}
        ifi.Type = uint8(data[0])
        binary.Read(bytes.NewBuffer(data[1:5]), binary.BigEndian, &ifi.Width)
        binary.Read(bytes.NewBuffer(data[5:9]), binary.BigEndian, &ifi.Height)
        binary.Read(bytes.NewBuffer(data[9:11]), binary.BigEndian, &ifi.Dq)
        binary.Read(bytes.NewBuffer(data[11:13]), binary.BigEndian, &ifi.Zs)
        ifi.Datas = data[13:]
        if len(ifi.Datas) == 0 {
            ifi.Datas = lastdatas[ifi.Dq]
        }
        return ifi
    }
    

    完整源码如下: server.go

    package main
    
    import (
    	"bufio"
    	"bytes"
    	"encoding/binary"
    	"errors"
    	"image"
    	"image/png"
    	"net"
    	"strconv"
    	"time"
    	"unsafe"
    
    	win "github.com/lxn/win"
    
    	"gitcode.com/jjgtmgx/mgxlog"
    )
    
    var Loger *mgxlog.MgxLog
    
    func main() {
    	Loger, _ = mgxlog.NewMgxLog("c:/runlog/", 10*1024*1024, 100, 3, 1000)
    	port := 1211
    	listener, err := net.Listen("tcp", ":"+strconv.Itoa(port))
    	if err != nil {
    		Loger.Errorf("Failed to listen on port: ", err)
    	}
    	Loger.Infof("Listening on port %d, waiting for image data...n", port)
    
    	// Receive and display the image
    	for {
    		conn, err := listener.Accept()
    		if err != nil {
    			Loger.Infof("Error accepting connection: ", err)
    			continue
    		}
    		go handleConnection(conn)
    	}
    }
    
    func handleConnection(conn net.Conn) {
    	defer conn.Close()
    	for {
    		reader := bufio.NewReader(conn)
    
    		// Read the length of the image data
    		info, err := reader.ReadString('n')
    		if err != nil {
    			Loger.Infof("read user info err: ", err)
    			return
    		}
    		if info != "administrator:mgx780707mgxn" {
    			Loger.Infof("user info err: ", info)
    			return
    		}
    		Loger.Infof("user login ok:", info)
    		captureScreenshots(conn)
    	}
    }
    
    func captureScreenshots(connection net.Conn) error {
    	for {
    		w, h, datas, err := Capture()
    		if err != nil {
    			Loger.Infof("capture err: %vn", err)
    			return err
    		}
    		err = sendBitmapData(connection, w, h, datas)
    		if err != nil {
    			Loger.Infof("send png err: %vn", err)
    			return err
    		}
    		// 休眠一段时间以控制截图频率
    		time.Sleep(41 * time.Millisecond)
    	}
    }
    
    var lastdatas = make(map[int][]byte)
    
    func sendBitmapData(conn net.Conn, w, h int, datas []byte) error {
    	// 计算每片数据的大小
    	count := (len(datas) + 999) / 1000
    	chunkSize := (len(datas) + count - 1) / count
    
    	// 将位图的宽度和高度转换为最节约的数据类型
    	width := uint32(w)
    	height := uint32(h)
    
    	// 分片发送位图数据
    	for i := 0; i < count; i++ {
    		// 计算当前片的起始位置和大小
    		start := i * chunkSize
    		end := start + chunkSize
    		if end > len(datas) {
    			end = len(datas)
    		}
    		// 获取当前片的数据
    		data := datas[start:end]
    		if ld, ok := lastdatas[i]; ok { //有老的对比
    			if bytes.Equal(ld, data) {
    				data = []byte{}
    			}
    		}
    		if len(data) > 0 {
    			lastdatas[i] = data
    		}
    		dataLen := int32(len(data))
    		totalsize := uint32(17 + dataLen)
    		ifi := ImgFpInfo{
    			Dsize:  totalsize,
    			Type:   1,
    			Width:  width,
    			Height: height,
    			Dq:     uint16(i),
    			Zs:     uint16(count),
    			Datas:  data,
    		}
    		// 发送当前片的数据
    		bs := ifi.GetBytes()
    		if _, err := conn.Write(bs); err != nil {
    			return err
    		}
    		//fmt.Println(ifi)
    	}
    	return nil
    }
    
    func Capture() (int, int, []byte, error) {
    	width := int(win.GetSystemMetrics(win.SM_CXSCREEN))
    	height := int(win.GetSystemMetrics(win.SM_CYSCREEN))
    	rect := image.Rect(0, 0, width, height)
    	img, err := CreateImage(rect)
    	if err != nil {
    		return width, height, nil, err
    	}
    	//hwnd := win.GetDesktopWindow()
    	hdc := win.GetDC(0)
    	if hdc == 0 {
    		return width, height, nil, errors.New("GetDC failed")
    	}
    	defer win.ReleaseDC(0, hdc)
    
    	memory_device := win.CreateCompatibleDC(hdc)
    	if memory_device == 0 {
    		return width, height, nil, errors.New("CreateCompatibleDC failed")
    	}
    	defer win.DeleteDC(memory_device)
    
    	bitmap := win.CreateCompatibleBitmap(hdc, int32(width), int32(height))
    	if bitmap == 0 {
    		return width, height, nil, errors.New("CreateCompatibleBitmap failed")
    	}
    	defer win.DeleteObject(win.HGDIOBJ(bitmap))
    
    	var header win.BITMAPINFOHEADER
    	header.BiSize = uint32(unsafe.Sizeof(header))
    	header.BiPlanes = 1
    	header.BiBitCount = 32
    	header.BiWidth = int32(width)
    	header.BiHeight = int32(-height)
    	header.BiCompression = win.BI_RGB
    	header.BiSizeImage = 0
    	bitmapDataSize := uintptr(((int64(width)*int64(header.BiBitCount) + 31) / 32) * 4 * int64(height))
    	hmem := win.GlobalAlloc(win.GMEM_MOVEABLE, bitmapDataSize)
    	defer win.GlobalFree(hmem)
    	memptr := win.GlobalLock(hmem)
    	defer win.GlobalUnlock(hmem)
    
    	old := win.SelectObject(memory_device, win.HGDIOBJ(bitmap))
    	if old == 0 {
    		return width, height, nil, errors.New("SelectObject failed")
    	}
    	defer win.SelectObject(memory_device, old)
    	if !win.BitBlt(memory_device, 0, 0, int32(width), int32(height), hdc, int32(0), int32(0), win.SRCCOPY) {
    		return width, height, nil, errors.New("BitBlt failed")
    	}
    	if win.GetDIBits(hdc, bitmap, 0, uint32(height), (*uint8)(memptr), (*win.BITMAPINFO)(unsafe.Pointer(&header)), win.DIB_RGB_COLORS) == 0 {
    		return width, height, nil, errors.New("GetDIBits failed")
    	}
    	i := 0
    	src := uintptr(memptr)
    	for y := 0; y < height; y++ {
    		for x := 0; x < width; x++ {
    			v0 := *(*uint8)(unsafe.Pointer(src))
    			v1 := *(*uint8)(unsafe.Pointer(src + 1))
    			v2 := *(*uint8)(unsafe.Pointer(src + 2))
    			img.Pix[i], img.Pix[i+1], img.Pix[i+2], img.Pix[i+3] = v2, v1, v0, 255
    			i += 4
    			src += 4
    		}
    	}
    	var buf bytes.Buffer
    	err = png.Encode(&buf, img)
    	if err != nil {
    		return width, height, nil, err
    	}
    	return width, height, buf.Bytes(), nil
    }
    
    func CreateImage(rect image.Rectangle) (img *image.RGBA, e error) {
    	img = nil
    	e = errors.New("Cannot create image.RGBA")
    	defer func() {
    		err := recover()
    		if err == nil {
    			e = nil
    		}
    	}()
    	img = image.NewRGBA(rect)
    	return img, e
    }
    
    type ImgFpInfo struct {
    	Dsize  uint32
    	Type   uint8
    	Width  uint32
    	Height uint32
    	Dq     uint16
    	Zs     uint16
    	Datas  []byte
    }
    
    func (ifi *ImgFpInfo) GetBytes() []byte {
    	b := bytes.NewBuffer([]byte{})
    	binary.Write(b, binary.BigEndian, ifi.Dsize)
    	binary.Write(b, binary.BigEndian, ifi.Type)
    	binary.Write(b, binary.BigEndian, ifi.Width)
    	binary.Write(b, binary.BigEndian, ifi.Height)
    	binary.Write(b, binary.BigEndian, ifi.Dq)
    	binary.Write(b, binary.BigEndian, ifi.Zs)
    	b.Write(ifi.Datas)
    	return b.Bytes()
    }
    
    

    client.go

    package main
    
    import (
    	"bytes"
    	"encoding/binary"
    	"fmt"
    	"log"
    	"net"
    	"os"
    
    	"github.com/gotk3/gotk3/gdk"
    	"github.com/gotk3/gotk3/glib"
    	"github.com/gotk3/gotk3/gtk"
    )
    
    var (
    	width  = 640
    	height = 480
    )
    
    func main() {
    	gtk.Init(nil)
    
    	window, _ := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
    	window.SetDefaultSize(800, 600)
    	window.Connect("destroy", func() {
    		gtk.MainQuit()
    	})
    
    	box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
    	window.Add(box)
    
    	image, _ := gtk.ImageNew()
    	box.PackStart(image, true, true, 0)
    
    	go updateImageAsync(image) // 异步更新图像
    
    	window.ShowAll()
    	gtk.Main()
    }
    
    // 异步更新图片
    func updateImageAsync(image *gtk.Image) {
    	// 服务器地址和端口
    	serverAddr := "192.168.2.26:1211"
    
    	// 连接到服务器
    	conn, err := net.Dial("tcp", serverAddr)
    	if err != nil {
    		log.Printf("Failed to connect to server: %vn", err)
    		os.Exit(1)
    	}
    	defer conn.Close()
    	conn.Write([]byte("administrator:mgx780707mgxn"))
    	BYTES_SIZE := 2048
    	HEAD_SIZE := 4
    	var (
    		buffer           = bytes.NewBuffer(make([]byte, 0, BYTES_SIZE))
    		bytes            = make([]byte, BYTES_SIZE)
    		isHead      bool = true
    		contentSize int
    		head        = make([]byte, HEAD_SIZE)
    		content     = make([]byte, BYTES_SIZE)
    	)
    	for {
    		readLen, err := conn.Read(bytes)
    		if err != nil {
    			log.Printf("read: %vn", err)
    			return
    		}
    		_, err = buffer.Write(bytes[0:readLen])
    		if err != nil {
    			log.Printf("read: %vn", err)
    			return
    		}
    		for {
    			if isHead {
    				if buffer.Len() >= HEAD_SIZE {
    					_, err := buffer.Read(head)
    					if err != nil {
    						log.Printf("read: %vn", err)
    						return
    					}
    					contentSize = int(binary.BigEndian.Uint32(head)) - HEAD_SIZE
    					isHead = false
    				} else {
    					break
    				}
    			}
    			if !isHead {
    				if buffer.Len() >= contentSize {
    					_, err := buffer.Read(content[:contentSize])
    					if err != nil {
    						log.Printf("read: %vn", err)
    						return
    					}
    					data := make([]byte, contentSize)
    					copy(data, content)
    					routeMessage(data, image)
    					isHead = true
    				} else {
    					break
    				}
    			}
    		}
    
    	}
    }
    
    func routeMessage(data []byte, image *gtk.Image) {
    	// 从socket读取PNG数据
    	ifi := dataToImgFpInfo(data)
    	//fmt.Println("read data:", ifi)
    	datas[ifi.Dq] = ifi.Datas
    	okhash[ifi.Dq] = true
    	if isok(int(ifi.Zs)) {
    		pngdata := make([]byte, 0)
    		for i := uint16(0); i < ifi.Zs; i++ {
    			pngdata = append(pngdata, datas[i]...)
    		}
    		lastdatas = datas
    		datas = make(map[uint16][]byte)
    		okhash = make(map[uint16]bool)
    		// 异步更新图像
    		glib.IdleAdd(func() {
    			loader, _ := gdk.PixbufLoaderNew()
    			loader.Write(pngdata)
    			loader.Close()
    			pixbuf, _ := loader.GetPixbuf()
    			image.SetSizeRequest(int(ifi.Width), int(ifi.Height))
    			// 将图像加载到图像控件
    			image.SetFromPixbuf(pixbuf)
    			image.QueueDraw()
    			fmt.Printf("Updated image: width=%d, height=%dn", ifi.Width, ifi.Height)
    		})
    	}
    
    }
    
    var lastdatas = make(map[uint16][]byte)
    var datas = make(map[uint16][]byte)
    var okhash = make(map[uint16]bool)
    
    func isok(t int) bool {
    	if len(okhash) != t {
    		return false
    	}
    	return true
    }
    
    func dataToImgFpInfo(data []byte) ImgFpInfo {
    	ifi := ImgFpInfo{}
    	ifi.Type = uint8(data[0])
    	binary.Read(bytes.NewBuffer(data[1:5]), binary.BigEndian, &ifi.Width)
    	binary.Read(bytes.NewBuffer(data[5:9]), binary.BigEndian, &ifi.Height)
    	binary.Read(bytes.NewBuffer(data[9:11]), binary.BigEndian, &ifi.Dq)
    	binary.Read(bytes.NewBuffer(data[11:13]), binary.BigEndian, &ifi.Zs)
    	ifi.Datas = data[13:]
    	if len(ifi.Datas) == 0 {
    		ifi.Datas = lastdatas[ifi.Dq]
    	}
    	return ifi
    }
    
    type ImgFpInfo struct {
    	Dsize  uint32
    	Type   uint8
    	Width  uint32
    	Height uint32
    	Dq     uint16
    	Zs     uint16
    	Datas  []byte
    }
    
    

    希望这篇文章能帮助你理解这个简单的远程传屏工具的实现原理。如果你有任何问题或者改进建议,欢迎在评论区留言!

    以上就是基于Go语言实现一个简易远程传屏工具的详细内容,更多关于Go远程传屏工具的资料请关注风君子博客其它相关文章!

    您可能感兴趣的文章:

    • Goland使用delve进行远程调试的详细教程
    • Goland远程连接Linux进行项目开发的实现
    • Go中RPC远程过程调用的实现
    • 详解Golang如何优雅接入多个远程配置中心
    • goland远程调试k8s上容器的实现

    站内搜索