最近需要在產品中加入桌面共享的功能,暫時不用實現遠程控制;參考了園子里的一些文章,加入了一些自己的修改。
需求:將一台機器的桌面通過網絡顯示到多個客戶端的屏幕上,顯示內容可能為PPT,Word文檔之類的內容,不含視頻。
1)抓屏
參考了網上找到的一段代碼如下
static BitmapSource CopyScreen() { using (var screenBmp = new Bitmap((int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) { using (var bmpGraphics = Graphics.FromImage(screenBmp)) { bmpGraphics.CopyFromScreen(0, 0, 0, 0, screenBmp.Size); return Imaging.CreateBitmapSourceFromHBitmap( screenBmp.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); } } }
看起來很簡潔,但是運行后,發現居然有內存泄漏,內存持續上漲,從30MB一直上漲到了1G多,還不停止,遂修改如下,杜絕了內存泄漏:
調用API中的DeleteObject
[System.Runtime.InteropServices.DllImport("gdi32.dll")] public static extern bool DeleteObject(IntPtr hObject);
private BitmapSource CopyScreen() { var handle = IntPtr.Zero; BitmapSource source = null; try { var bitmap = new Bitmap((int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight); var g = Graphics.FromImage(bitmap); g.CopyFromScreen(0, 0, 0, 0, bitmap.Size); handle = bitmap.GetHbitmap(); source = Imaging.CreateBitmapSourceFromHBitmap(handle, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); } finally { DeleteObject(handle); } return source; }
這樣處理后,抓屏不再造成內存泄漏,內存占用穩定不再增長 ;
2)傳輸
因為共享客戶端可能是一對多,所以不能再采用TCP點對點傳輸,嘗試使用UDP組播的方式來傳輸數據,遇到了不少問題;
UDP組播的方法就不再贅述,使用組播的好處在於服務端的網絡帶寬占用不會因為客戶端的增加而增加。
問題一:UDP報文有最大長度限制,所以無法一次發送一張圖片
解決方案:自行切分傳輸,在接收端重組,定義報文類
class Packet { public int SN {get;set;} public byte[] Data {get;set;} public byte[] ToBytes() {....} }
切分報文與重組報文,就不再詳述。報文發送完畢之后,發送一個END標志,接收端重組報文,再顯示圖像。本機測試OK,然后放到客戶端時,問題出來了。
問題 二:UDP本身是不可靠傳輸,所以報文到達的順序可能會亂,也有報文會丟失
解決方案:每一張圖片,給予一個GUID,並加上一個時間戳;接收端采用具備緩沖區的接收方式;工作方式如下:
1)發送端:每一個時間間隔抓取一張圖片,生成一個Guid,然后進行報文分割,再將報文發送到組播地址,每個報文包含GUID、報文數量、報文序號、時間戳以及數據;
2)接收端:
a)接收到一個報文后,放入報文池;
b)丟棄超時的報文(檢查時間戳,超過某個時間的報文丟棄,例如5秒,這種應用不要求嚴格的數據完整性,收不到的,就丟棄)
c)檢查緩沖區內的所有報文,檢查將時間最早的報文,並檢查其完整性(若報文應該有三個,是否已經收到三個同時具備該GUID的報文),若該GUID的報文已完整,則觸發圖片准備好的事件(ImageReady),並刪除該圖片的所有報文數據;
d)客戶端通過ImageReady事件顯示圖像;
測試結果:本機測試OK,局域網測試又失敗了,查看調試信息發現客戶端總是只能收到所有報文中的第一個@……*#……@,其他的都丟失了~
問題三:局域網上UDP報文丟失?@#
解決方案:發送報文時,加入一定的時間間隔。這個是試出來的,原因可能是發太快了,網絡傳輸不了,就丟失了;
最終代碼結構如下:
報文類:
class Packet { public double Time {get;set;} public int Total {get;set;} public int SN {get;set;} public byte[] Data {get;set;} public Packet() {...} public byte[] ToBytes() {....} public static Packet FromBytes(byte[] bytes){...} }
public ShareServer { public ShareServer() { }
// 偽代碼,非可運行代碼 public void Start() { 1)初始化UdpClient,進行組播 // 2)開一個線程,進入循環 while (_doing) { var source = CopyScreen(); if (source == null) continue; var guid = Guid.NewGuid(); // JPEG 編碼 var enc = new JpegBitmapEncoder() { QualityLevel = 40 }; var ms = new MemoryStream(); enc.Frames.Add(BitmapFrame.Create(source)); enc.Save(ms); // 壓縮MemoryStream // GZipStream var data = Compress(ms.ToArray()); // 分割報文 var packages = splitPackagets(data, guid); // 傳輸報文 foreach (var packaget in packages) { var bytesArray = packaget.ToBytes(); _udpClient.Send(packaget.ToBytes(), bytesArray.Length); // 加入一個間隔 Thread.Sleep(100); } // 1 秒抓一次圖 Thread.Sleep(1000); } } }
接收端:
class BufferedScreenClient { public event EventHandler<ImageReadyEventArgs> ImageReady; private List<Packet> _packets; public BufferedScreenClient() {......} public void AddPacket(Packet packet) { 1)添加報文 2)清理超時報文 3)檢查最早的一張圖片是否已經OK,如果OK觸發ImageReady事件 a) 首先查詢出這一組的報文 b)合並這一組報文,獲取數據 c)解壓縮 d)JPEG解碼成BitmapSource e)移除該圖片的報文數據 f)通過事件,將圖片發送給接收端進行顯示 _packagets.Add(packet); CleanPackets(); var source = GetFirstImage(); if (source != null) { OnImageReady(source); } }
private void CleanPackets(){....}
private void MergePackets(IEnuerable<Packet> packets){.....}
private BitmapSource GetFirstImage(){......}
}
經測試,一台服務端+兩台客戶端均可接收到圖片,但是第一張圖出來的比較慢,網絡發送這一塊,還需要進行優化。圖片質量與發送間隔,可根據網絡狀況進行調整;
TODO List
1)優化報文發送與報文接收機制
2)圖片分塊發送
3)比較兩次截圖之間的差異,如圖片沒變化就不發送
4)優化接收端的機制
參考:
1)暮雨冰藍 的C#局域網共享系列文章 http://www.cnblogs.com/liuxiaobo93/p/3675387.html
2)C# 組播
3)WPF 下 抓屏等