基于Go語言實現(xiàn)一個簡易遠程傳屏工具
項目架構(gòu):簡單到「令人發(fā)指」
這個項目的架構(gòu)簡單得不能再簡單了:一個服務(wù)端,一個客戶端。服務(wù)端負(fù)責(zé)截圖并發(fā)送,客戶端負(fù)責(zé)接收并顯示。就像兩個人打電話,一個說,一個聽,完美配合!
讓我們先看看這個項目的目錄結(jié)構(gòu):
遠程傳屏/
├── client/
│ ├── main.go # 客戶端代碼
│ └── go.mod # 客戶端依賴
└── server/
├── main.go # 服務(wù)端代碼
└── go.mod # 服務(wù)端依賴
服務(wù)端:「咔嚓」一聲,屏幕被我「抓」住了
服務(wù)端的主要工作就是不斷地「咔嚓咔嚓」截圖,然后把圖片發(fā)送給客戶端。讓我們看看服務(wù)端的核心代碼:
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)
// 循環(huán)接受連接
for {
conn, err := listener.Accept()
if err != nil {
Loger.Infof("Error accepting connection: ", err)
continue
}
go handleConnection(conn)
}
}
服務(wù)端首先啟動一個TCP監(jiān)聽,然后等待客戶端連接。注意這里用了goroutine來處理每個連接,這樣就可以同時服務(wù)多個客戶端了!
接下來是身份驗證部分:
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)
}
}
這里有個簡單的身份驗證機制,客戶端必須發(fā)送正確的用戶名和密碼才能繼續(xù)。不過說實話,這個密碼直接硬編碼在代碼里,安全性嘛... 咱們就當(dāng)是內(nèi)部工具,別太較真~
最核心的截圖功能在這里:
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
}
服務(wù)端使用Windows API來捕獲整個屏幕,然后將截圖編碼為PNG格式。這里還有個小優(yōu)化:
// 計算每片數(shù)據(jù)的大小
count := (len(datas) + 999) / 1000
chunkSize := (len(datas) + count - 1) / count
// ... [分片發(fā)送代碼]
if ld, ok := lastdatas[i]; ok { //有老的對比
if bytes.Equal(ld, data) {
data = []byte{}
}
}
看到了嗎?代碼會記錄上一次發(fā)送的數(shù)據(jù),如果當(dāng)前分片和上次一樣,就發(fā)送一個空數(shù)據(jù)塊。這樣可以節(jié)省帶寬,特別是當(dāng)屏幕大部分內(nèi)容沒有變化的時候!
客戶端:「看,屏幕飛過來了!」
客戶端的工作相對簡單一些:連接服務(wù)器,接收圖片數(shù)據(jù),然后顯示出來。讓我們看看客戶端的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庫創(chuàng)建了一個簡單的窗口,里面放了一個圖像控件。然后啟動一個goroutine來異步接收和更新圖像,這樣就不會阻塞UI線程了。
接收數(shù)據(jù)的邏輯在這里:
func updateImageAsync(image *gtk.Image) {
// 服務(wù)器地址和端口
serverAddr := "192.168.2.26:1211"
// 連接到服務(wù)器
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"))
// ... [數(shù)據(jù)接收和處理代碼] ...
}
客戶端首先連接到服務(wù)器,發(fā)送用戶名密碼,然后開始接收數(shù)據(jù)。當(dāng)接收到完整的圖像數(shù)據(jù)后,就更新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的主事件循環(huán)中更新UI,這是GUI編程的常見做法。
數(shù)據(jù)傳輸協(xié)議:簡單但有效
這個項目定義了一個簡單的數(shù)據(jù)結(jié)構(gòu)來傳輸圖像信息:
type ImgFpInfo struct {
Dsize uint32 // 數(shù)據(jù)大小
Type uint8 // 數(shù)據(jù)類型
Width uint32 // 圖像寬度
Height uint32 // 圖像高度
Dq uint16 // 當(dāng)前數(shù)據(jù)塊序號
Zs uint16 // 總數(shù)據(jù)塊數(shù)量
Datas []byte // 實際圖像數(shù)據(jù)
}
這個結(jié)構(gòu)包含了圖像的基本信息,以及數(shù)據(jù)分塊的信息。服務(wù)端將圖像分成多個小塊發(fā)送,客戶端接收后再重新組裝起來。
小結(jié):簡單實用的小工具
這個遠程傳屏工具雖然簡單,但功能完整,而且有一些不錯的優(yōu)化:
- 使用TCP保證數(shù)據(jù)傳輸?shù)目煽啃?/li>
- 增量更新,只發(fā)送變化的部分
- 數(shù)據(jù)分塊傳輸,避免大文件傳輸問題
- 異步處理,保證UI流暢
當(dāng)然,這個工具還有很多可以改進的地方,比如:
- 更安全的身份驗證機制
- 加密傳輸數(shù)據(jù)
- 支持多屏幕選擇
- 增加控制功能(比如遠程操作)
不過,作為一個簡單的遠程傳屏工具,它已經(jīng)能夠滿足基本需求了。如果你有興趣,可以基于這個代碼進行擴展和改進!
最后,附上項目中使用的自定義數(shù)據(jù)結(jié)構(gòu)和工具函數(shù),方便大家理解整個數(shù)據(jù)流:
// 服務(wù)器端的工具方法
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()
}
// 客戶端的數(shù)據(jù)解析方法
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: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 {
// 計算每片數(shù)據(jù)的大小
count := (len(datas) + 999) / 1000
chunkSize := (len(datas) + count - 1) / count
// 將位圖的寬度和高度轉(zhuǎn)換為最節(jié)約的數(shù)據(jù)類型
width := uint32(w)
height := uint32(h)
// 分片發(fā)送位圖數(shù)據(jù)
for i := 0; i < count; i++ {
// 計算當(dāng)前片的起始位置和大小
start := i * chunkSize
end := start + chunkSize
if end > len(datas) {
end = len(datas)
}
// 獲取當(dāng)前片的數(shù)據(jù)
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,
}
// 發(fā)送當(dāng)前片的數(shù)據(jù)
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) {
// 服務(wù)器地址和端口
serverAddr := "192.168.2.26:1211"
// 連接到服務(wù)器
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數(shù)據(jù)
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
}
希望這篇文章能幫助你理解這個簡單的遠程傳屏工具的實現(xiàn)原理。如果你有任何問題或者改進建議,歡迎在評論區(qū)留言!
以上就是基于Go語言實現(xiàn)一個簡易遠程傳屏工具的詳細內(nèi)容,更多關(guān)于Go遠程傳屏工具的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
CMD下執(zhí)行Go出現(xiàn)中文亂碼的解決方法
需要在Go寫的服務(wù)里面調(diào)用命令行或者批處理,并根據(jù)返回的結(jié)果做處理。但是windows下面用cmd返回中文會出現(xiàn)亂碼,本文就詳細的介紹一下解決方法,感興趣的可以了解一下2021-12-12
Go中的new()和make()函數(shù)區(qū)別及底層原理詳解
這篇文章主要為大家介紹了Go中的new()和make()函數(shù)區(qū)別及底層原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-09-09
Go語言中io.Reader和io.Writer的詳解與實現(xiàn)
在Go語言的實際編程中,幾乎所有的數(shù)據(jù)結(jié)構(gòu)都圍繞接口展開,接口是Go語言中所有數(shù)據(jù)結(jié)構(gòu)的核心。在使用Go語言的過程中,無論你是實現(xiàn)web應(yīng)用程序,還是控制臺輸入輸出,又或者是網(wǎng)絡(luò)操作,不可避免的會遇到IO操作,使用到io.Reader和io.Writer接口。下面來詳細看看。2016-09-09

