当前位置: 首页 > news >正文

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

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

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

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

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

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

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

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

func main() {Loger, _ = mgxlog.NewMgxLog("c:/runlog/", 10*1024*1024, 100, 3, 1000)port := 1211listener, 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:mgx780707mgx\n" {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.Buffererr = 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: %v\n", err)os.Exit(1)}defer conn.Close()conn.Write([]byte("administrator:mgx780707mgx\n"))// ... [数据接收和处理代码] ...
}

客户端首先连接到服务器,发送用户名密码,然后开始接收数据。当接收到完整的图像数据后,就更新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=%d\n", 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 mainimport ("bufio""bytes""encoding/binary""errors""image""image/png""net""strconv""time""unsafe"win "github.com/lxn/win""gitcode.com/jjgtmgx/mgxlog"
)var Loger *mgxlog.MgxLogfunc main() {Loger, _ = mgxlog.NewMgxLog("c:/runlog/", 10*1024*1024, 100, 3, 1000)port := 1211listener, 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 imagefor {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 datainfo, 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) / 1000chunkSize := (len(datas) + count - 1) / count// 将位图的宽度和高度转换为最节约的数据类型width := uint32(w)height := uint32(h)// 分片发送位图数据for i := 0; i < count; i++ {// 计算当前片的起始位置和大小start := i * chunkSizeend := start + chunkSizeif 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.BITMAPINFOHEADERheader.BiSize = uint32(unsafe.Sizeof(header))header.BiPlanes = 1header.BiBitCount = 32header.BiWidth = int32(width)header.BiHeight = int32(-height)header.BiCompression = win.BI_RGBheader.BiSizeImage = 0bitmapDataSize := 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 := 0src := 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, 255i += 4src += 4}}var buf bytes.Buffererr = 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 = nile = 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  uint32Type   uint8Width  uint32Height uint32Dq     uint16Zs     uint16Datas  []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 mainimport ("bytes""encoding/binary""fmt""log""net""os""github.com/gotk3/gotk3/gdk""github.com/gotk3/gotk3/glib""github.com/gotk3/gotk3/gtk"
)var (width  = 640height = 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 := 2048HEAD_SIZE := 4var (buffer           = bytes.NewBuffer(make([]byte, 0, BYTES_SIZE))bytes            = make([]byte, BYTES_SIZE)isHead      bool = truecontentSize inthead        = 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_SIZEisHead = 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.Datasokhash[ifi.Dq] = trueif isok(int(ifi.Zs)) {pngdata := make([]byte, 0)for i := uint16(0); i < ifi.Zs; i++ {pngdata = append(pngdata, datas[i]...)}lastdatas = datasdatas = 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  uint32Type   uint8Width  uint32Height uint32Dq     uint16Zs     uint16Datas  []byte
}

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

往期部分文章列表

  • 当你的程序学会了“诈尸“:Go 实现 Windows 进程守护术
  • 验证码识别API:告别收费接口,迎接免费午餐
  • 用 Go 给 Windows 装个"顺风耳":两分钟写个录音小工具
  • 无奈!我用go写了个MySQL服务
  • 使用 Go + govcl 实现 Windows 资源管理器快捷方式管理器
  • 用 Go 手搓一个 NTP 服务:从"时间混乱"到"精准同步"的奇幻之旅
  • 用 Go 手搓一个 Java 构建工具:当 IDE 不在身边时的自救指南
  • 深入理解 Windows 全局键盘钩子(Hook):拦截 Win 键的 Go 实现
  • 用 Go 语言实现《周易》大衍筮法起卦程序
  • Go 语言400行代码实现 INI 配置文件解析器:支持注释、转义与类型推断
  • 高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全
  • Golang + OpenSSL 实现 TLS 安全通信:从私有 CA 到动态证书加载
http://www.dtcms.com/a/527966.html

相关文章:

  • 哪些网站做翻译可以赚钱织梦网站更改标题长度
  • 阮一峰《TypeScript 教程》学习笔记——declare关键字
  • Flutter 异步编程:Future 与 Stream 深度解析
  • 代码训练LeetCode(48)字母异位词分组
  • 每日算法刷题Day79:10.25:leetcode 一般树5道题,用时1h30min
  • 数据分析核心术语略解
  • 南宁网站设计和开发大赛诚信通开了网站谁给做
  • MATLAB基于云模型时间序列预测
  • 【成长纪实】HarmonyOS Next学习地图:新手避坑指南与核心知识点拆解
  • wordpress不适合大型网站网站建设对宣传的意义
  • 大良营销网站建设教程写网站建设需求文档
  • CICD实战(13) - 使用Arbess+GitLab实现.Net core项目自动化部署
  • KingbaseES赋能多院区医院信创转型:浙江省人民医院异构多活数据底座实践解析
  • 微硕WSF2N65 650V N沟MOSFET:汽车PTC辅助加热器“高压启动核”
  • 如何在Mac进行Safari网页长截图?
  • 【2026计算机毕业设计】基于Jsp的汽车租赁信息管理系统
  • LLMs之PE:PromptX(将 AI 智能体从通用助手转变为具备行业/角色能力的交互平台)的简介、安装和使用方法、案例应用之详细攻略
  • AI驱动的DevOps:AI大模型自动化部署、监控和运维流程
  • 汽车销售|汽车推荐|基于SprinBoot+vue的新能源汽车个性化推荐系统(源码+数据库+文档)
  • 佳易王试用版汽车保养维修服务记录查询系统 V17.1:提升汽修门店管理效率的实用工具#汽修保养
  • 我的firefox的新建标签+按钮找不到了
  • 网站建设方案 下载开个网站需要什么
  • 揭阳网站建设维护网站建设 教程
  • HTTP方法的安全性和幂等性
  • HTTP、HTTPS 和 WebSocket 协议和开发
  • 动态规划详细题解——力扣198.打家劫舍
  • 【LeetCode热题100(52/100)】课程表
  • 什么行业必须做网站棋牌软件开发一个多少钱
  • LeetCode:698. 划分为k个相等的子集
  • 【LeetCode100】--- 101.重排链表【思维导图+复习回顾】