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

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

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

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

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

复制代码
远程传屏/
├── 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
}

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

往期部分文章列表

相关推荐
九江Mgx6 小时前
自由通讯的魔法:Go从零实现UDP/P2P 聊天工具
golang·udp·p2p
ErizJ6 小时前
IM|im-service
golang·kafka·go·im·心跳检测
绛洞花主敏明10 小时前
Gorm(六)错误处理 & RowsAffected
golang
kgduu15 小时前
go-ethereum core之交易索引txIndexer
服务器·数据库·golang
ALex_zry17 小时前
构建通用并发下载工具:用Golang重构wget脚本的实践分享
开发语言·重构·golang
Dobby_0517 小时前
【Go】C++ 转 Go 第(五)天:Goroutine 与 Channel | Go 并发编程基础
vscode·golang
Moshow郑锴18 小时前
Oracle CLOB中包含不可见的控制字符导致golang中json转换失败问题
oracle·golang·json
妮妮喔妮1 天前
Go的垃圾回收
开发语言·后端·golang
golang学习记1 天前
Go slog 日志打印最佳实践指南
开发语言·后端·golang