Go语言实现的简易远程传屏工具:让你的屏幕「飞」起来

大家好!今天我要给大家介绍一个用Go语言写的「远程传屏工具」------ 这可不是什么高大上的商业软件,而是一个非常实用的小工具,让你的屏幕内容能够轻松「飞」到另一台电脑上。

项目架构:简单到「令人发指」

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

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

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

服务端:「咔嚓」一声,屏幕被我「抓」住了

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

go 复制代码
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来处理每个连接,这样就可以同时服务多个客户端了!

接下来是身份验证部分:

go 复制代码
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:mgx780707mgx\n" {
            Loger.Infof("user info err: ", info)
            return
        }
        Loger.Infof("user login ok:", info)
        captureScreenshots(conn)
    }
}

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

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

go 复制代码
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格式。这里还有个小优化:

go 复制代码
// 计算每片数据的大小
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部分:

go 复制代码
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线程了。

接收数据的逻辑在这里:

go 复制代码
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: %v\n", err)
        os.Exit(1)
    }
    defer conn.Close()
    conn.Write([]byte("administrator:mgx780707mgx\n"))
    // ... [数据接收和处理代码] ...
}

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

go 复制代码
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=%d\n", ifi.Width, ifi.Height)
})

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

数据传输协议:简单但有效

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

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

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

小结:简单实用的小工具

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

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

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

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

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

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

go 复制代码
// 服务器端的工具方法
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

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:mgx780707mgx\n" {
			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: %v\n", err)
			return err
		}
		err = sendBitmapData(connection, w, h, datas)
		if err != nil {
			Loger.Infof("send png err: %v\n", 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

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: %v\n", err)
		os.Exit(1)
	}
	defer conn.Close()
	conn.Write([]byte("administrator:mgx780707mgx\n"))
	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: %v\n", err)
			return
		}
		_, err = buffer.Write(bytes[0:readLen])
		if err != nil {
			log.Printf("read: %v\n", err)
			return
		}
		for {
			if isHead {
				if buffer.Len() >= HEAD_SIZE {
					_, err := buffer.Read(head)
					if err != nil {
						log.Printf("read: %v\n", 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: %v\n", 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=%d\n", 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
}

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

往期部分文章列表

相关推荐
梦想很大很大9 小时前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰14 小时前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘17 小时前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤18 小时前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt111 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto4 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室5 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题5 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉7 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想