35 / 52 主插件与远程能力

第 35 课:主控屏幕墙与加入监控

# 第 35 课:主控屏幕墙与加入监控

免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。

本课只讲主控侧“屏幕墙/加入监控”的代码结构和检测点,不讲后台屏幕、后台桌面或后台窗口的采集实现,也不提供未授权监控操作步骤。

# 1. 先明确边界

屏幕墙不是后台屏幕,也不是后台桌面/后台窗口的一部分。它只是专栏顺序上放在屏幕类专栏之后,方便读者理解“单个屏幕窗口”和“多目标监控面板”的区别。

功能 关注点 本课是否讲
高速屏幕/差异屏幕/娱乐屏幕 被控端如何采集、编码、传输屏幕 不展开
后台桌面/后台窗口 后台桌面、窗口绘制和能力限制 不展开
屏幕墙/加入监控 主控如何把多个被控加入监控面板、定时请求缩略图并集中显示 本课重点

屏幕墙可以理解成主控侧的“多目标缩略图看板”。它关心的是:哪些被控在列表里、多久刷新一次、收到缩略图后画到哪里、退出监控时如何清理状态。

1. 先明确边界(配图)

# 2. 涉及源码

文件 作用
主控\Quick\QuickView.cpp 主连接列表右键菜单,“加入监控”“退出监控”的入口
主控\Quick\ScreenMonitorDlg.cpp 屏幕墙主实现,添加/删除客户、定时请求、接收缩略图、右键操作
主控\Quick\ScreenMonitorDlg.h 屏幕墙类定义和消息处理入口
主控\Quick\MainFrm.cpp 创建屏幕监控停靠窗格、处理 TOKEN_MONITOR 返回
主控\Quick\macros.h COMMAND_MONITORCOMMAND_GETMONITORTOKEN_MONITORMONITOR_DLG 等常量
主插件\登录模块\登录模块\LoginManager.cpp 被控登录模块收到 COMMAND_MONITOR 后回传监控令牌

# 3. 主控界面入口:加入监控和退出监控

主连接列表右键菜单中有“加入监控”和“退出监控”。这两个入口只负责改变屏幕墙列表和触发监控命令,不涉及后台桌面技术。

// 源码位置:主控\Quick\QuickView.cpp:373-374
// 防御分析:这是主连接列表上的屏幕墙入口。
// 它和后台屏幕/后台桌面没有直接实现关系,只是把被控加入主控侧监控面板。
mListmeau.AppendMenu(MF_STRING | MF_ENABLED, MENU_加入监控, _T("加入监控"));
mListmeau.AppendMenu(MF_STRING | MF_ENABLED, MENU_退出监控, _T("退出监控"));
1
2
3
4
5

菜单处理逻辑如下。

// 源码位置:主控\Quick\QuickView.cpp:1189-1207
case MENU_加入监控:
{
    // 防御分析:COMMAND_MONITOR 是加入屏幕墙的启动命令。
    // 它会发给当前选中的被控,要求对方进入监控回传链路。
    BYTE bToken = COMMAND_MONITOR;
    SendSelectCommand(&bToken, sizeof(BYTE));
    log_信息("加入监控");
}
break;

case MENU_退出监控:
{
    CXTPReportSelectedRows* pSelRows = wndReport->GetSelectedRows();
    POSITION pSelRows_POSITION = pSelRows->GetFirstSelectedRowPosition();
    while (pSelRows_POSITION)
    {
        CXTPReportRow* pSelRow_choose = pSelRows->GetNextSelectedRow(pSelRows_POSITION);
        if (pSelRow_choose == nullptr) return;
        if (pSelRow_choose->IsGroupRow()) return;

        ClientContext* pContext = (ClientContext*)(((pSelRow_choose->GetRecord())->GetItem(0))->GetItemData());
        if (pContext)
        {
            // 防御分析:退出监控不发采集命令,而是通知 ScreenMonitorDlg 删除这个上下文。
            g_pScreenMonitorDlg->SendMessage(MONITOR_DLG, 0, (LPARAM)pContext);
            if (pContext->Item_cmp_old_IsActive)
                pContext->Item_cmp_old_IsActive->SetBackgroundColor(4294967295);
        }
    }
    log_信息("退出监控");
}
break;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 4. 被控如何回应加入监控

COMMAND_MONITOR 到达登录模块后,登录模块会返回 TOKEN_MONITOR。这里仍然不是具体屏幕采集,只是把被控加入监控链路。

// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:436-454
case COMMAND_MONITOR:
{
    // 防御分析:收到主控的加入监控命令后,被控回一个 TOKEN_MONITOR。
    // 主控收到这个 token 后,会把对应 ClientContext 加入 ScreenMonitorDlg。
    BYTE lpPacket = TOKEN_MONITOR;
    Send((LPBYTE)&lpPacket, 1);
}
break;
1
2
3
4
5
6
7
8
9

主控网络回调收到 TOKEN_MONITOR 后,把上下文投递给屏幕墙窗口。

// 源码位置:主控\Quick\MainFrm.cpp:2027-2029
case TOKEN_MONITOR: // 屏幕监控
    // 防御分析:主控收到被控确认后,交给 ScreenMonitorDlg 处理。
    g_pScreenMonitorDlg->PostMessage(WM_MONITOR_CLIENT, 0, (LPARAM)pContext);
    break;
1
2
3
4
5

4. 被控如何回应加入监控(配图)

# 5. ScreenMonitorDlg 如何添加客户

ScreenMonitorDlg 收到消息后,如果当前数据缓冲区没有图片,就调用 AddClient 把被控加入列表。

// 源码位置:主控\Quick\ScreenMonitorDlg.cpp:190-203
afx_msg LRESULT CScreenMonitorDlg::OnAddClientData(WPARAM wParam, LPARAM lParam)
{
    ClientContext* pContext = (ClientContext*)lParam;

    if (pContext->m_DeCompressionBuffer.GetBufferLen() > 2)
    {
        // 防御分析:如果已经带有图片数据,就进入缩略图更新逻辑。
        OnReceiveComplete(pContext);
    }
    else
    {
        // 防御分析:第一次加入时通常没有图片,先登记到屏幕墙列表。
        AddClient(pContext);
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

AddClient 的作用是把 ClientContext 放入列表控件,标记这个连接已经属于 MONITOR_DLG,并更新状态栏计数。

// 源码位置:主控\Quick\ScreenMonitorDlg.cpp:255-271
VOID CScreenMonitorDlg::AddClient(ClientContext* pContext)
{
    // 防御分析:防止重复加入屏幕墙。
    if (pContext->m_Dialog[1] == MONITOR_DLG)
        return;

    // 防御分析:m_Dialog[1] 是主控侧功能占用标记,后续断线和重复登录会用它判断是否在监控面板中。
    pContext->m_Dialog[1] = MONITOR_DLG;

    int nCnt = m_listCtrl.GetItemCount();
    m_listCtrl.InsertItem(nCnt, pContext->szAddress, nCnt);
    m_listCtrl.SetItemData(nCnt, (DWORD_PTR)pContext);

    // 防御分析:主连接列表中把已监控目标标绿色,便于操作者识别。
    if (pContext->Item_cmp_old_IsActive)
        pContext->Item_cmp_old_IsActive->SetBackgroundColor(RGB(150, 212, 134));

    CBitmap bmp;
    bmp.LoadBitmap(IDB_BIGFIRE);
    m_ImageList.Add(&bmp, RGB(255, 255, 255));

    CString str;
    str.Format(_T("监控:%d"), m_listCtrl.GetItemCount());
    g_pFrame->m_wndStatusBar.SetPaneText(10, str);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 6. 屏幕墙如何定时请求缩略图

屏幕墙用定时器定期向列表里的被控发送 COMMAND_GETMONITOR,并附带期望缩略图宽高。

// 源码位置:主控\Quick\ScreenMonitorDlg.cpp:407-430
void CScreenMonitorDlg::OnTimer(UINT_PTR nIDEvent)
{
    if (nIDEvent == 1 && g_pScreenMonitorDlg->IsWindowVisible())
    {
        // 防御分析:包格式为 COMMAND_GETMONITOR + 宽度 + 高度。
        // 这是屏幕墙缩略图请求,不等于后台桌面采集协议。
        byte* lpbyte = new byte[9];
        lpbyte[0] = COMMAND_GETMONITOR;
        memcpy(lpbyte + 1, &m_w, 4);
        memcpy(lpbyte + 5, &m_h, 4);

        int nCnt = m_listCtrl.GetItemCount();
        for (int i = 0; i < nCnt; i++)
        {
            m_listCtrl.GetItemRect(i, &m_rect, 0);

            // 防御分析:只向当前可见区域内的项目请求缩略图,减少无效刷新。
            if (m_rect.left > mcx)
                continue;
            if (m_rect.top > mcy)
                continue;

            ClientContext* pContext_old = (ClientContext*)m_listCtrl.GetItemData(i);
            if (pContext_old)
                g_pSocketBase->Send(pContext_old, lpbyte, 9);
        }
    }
    CXTPResizeFormView::OnTimer(nIDEvent);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

这段逻辑说明屏幕墙的核心不是“采集屏幕”,而是“主控侧定时向多个目标请求缩略图,然后集中显示”。

# 7. 缩略图数据如何采集

理解屏幕墙时,新手最容易漏掉一个问题:ScreenMonitorDlg::OnTimer 只是发请求,真正的缩略图数据是在被控端哪里生成的?

答案在登录模块。主控发出的 COMMAND_GETMONITOR 会进入 CLoginManager::OnReceive,登录模块读取主控给出的宽高,然后调用 GetScreen(..., setsize=true, w, h) 生成一张小尺寸当前桌面截图。也就是说,屏幕墙缩略图不是后台桌面数据,也不是差异屏幕数据,而是登录模块临时抓取的一张缩放后的当前桌面 JPEG。

7. 缩略图数据如何采集(配图)

先看命令处理段。

// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:443-460
case COMMAND_GETMONITOR:
{
    int w = 300, h = 200;

    // 主控屏幕墙请求包格式是:
    // [0]      COMMAND_GETMONITOR
    // [1..4]   缩略图宽度
    // [5..8]   缩略图高度
    // 防御分析:宽高来自主控侧,看起来像普通参数,但它直接影响被控端截图尺寸、
    // 内存分配和回传包大小。审计时要确认是否做了合理范围限制。
    memcpy(&w, lpBuffer + 1, 4);
    memcpy(&h, lpBuffer + 5, 4);

    int screendatasize = 0;
    int x = 0;
    int y = 0;

    // setsize=true 表示按主控给出的 w/h 生成缩略图。
    // blitter=false 表示这里不是登录模块状态更新里的 1/10 小图路径。
    // 防御分析:这是当前桌面快照,不是后台桌面,也不是差异屏幕的增量帧。
    byte* screendata = GetScreen(screendatasize, x, y, false, true, w, h);

    int SendSize = screendatasize + 5;
    BYTE* lpPacket = new BYTE[SendSize];

    // 回包格式:
    // [0]      TOKEN_MONITOR
    // [1..4]   JPEG 数据长度
    // [5..]    JPEG 缩略图数据
    // 主控的 ScreenMonitorDlg::OnReceiveComplete 会跳过前 5 字节,再用 CImage 加载图片。
    lpPacket[0] = TOKEN_MONITOR;
    memcpy(lpPacket + 1, &screendatasize, 4);
    ::memcpy(lpPacket + 5, screendata, screendatasize);

    Send((LPBYTE)lpPacket, SendSize);
    SAFE_DELETE_AR(screendata);
    SAFE_DELETE_AR(lpPacket);
}
break;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

再看 GetScreen 的核心。它没有进入后台桌面,也没有启动某个屏幕插件,而是直接对当前虚拟桌面做 GDI 截图和缩放。

// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:729-829
byte* CLoginManager::GetScreen(
    int& size,
    int& x,
    int& y,
    bool blitter,
    bool setsize,
    int setw,
    int seth)
{
    // 取当前桌面窗口 DC。这里对应的是当前会话里的虚拟桌面,
    // 不涉及第 34 课讲的后台桌面 CreateDesktop/SwitchDesktop 链路。
    HDC hCurrScreen = ::GetDC(GetDesktopWindow());
    HDC hCmpDC = CreateCompatibleDC(hCurrScreen);

    // 源码用 DESKTOPHORZRES/HORZRES 计算 DPI 缩放比例,
    // 目的是让高 DPI 环境下的虚拟屏幕尺寸更接近真实像素尺寸。
    HDC hdc = GetDC(NULL);
    int client_width_now = GetDeviceCaps(hdc, HORZRES);
    int client_width_old = GetDeviceCaps(hdc, DESKTOPHORZRES);
    float fDpiRatio = (float)client_width_old / (float)client_width_now;
    ReleaseDC(NULL, hdc);

    int iScreenWidth = 0;
    int iScreenHeight = 0;

    if (setsize)
    {
        // 屏幕墙路径:主控指定缩略图宽高,例如 300x200。
        // 防御分析:这会显著降低回传数据量,但仍然属于屏幕内容采集。
        iScreenWidth = setw;
        iScreenHeight = seth;
    }
    else if (blitter)
    {
        // 登录状态更新路径:用虚拟屏幕尺寸的 1/10 生成小图。
        iScreenWidth = (int)((float)(::GetSystemMetrics(SM_CXVIRTUALSCREEN)) * fDpiRatio) / 10;
        iScreenHeight = (int)((float)(::GetSystemMetrics(SM_CYVIRTUALSCREEN)) * fDpiRatio) / 10;
    }
    else
    {
        // 普通截图路径:使用完整虚拟屏幕尺寸。
        iScreenWidth = (int)((float)(::GetSystemMetrics(SM_CXVIRTUALSCREEN)) * fDpiRatio);
        iScreenHeight = (int)((float)(::GetSystemMetrics(SM_CYVIRTUALSCREEN)) * fDpiRatio);
    }

    int iScreenX = (int)((float)(::GetSystemMetrics(SM_XVIRTUALSCREEN)) * fDpiRatio);
    int iScreenY = (int)((float)(::GetSystemMetrics(SM_YVIRTUALSCREEN)) * fDpiRatio);

    HBITMAP hBmp = CreateCompatibleBitmap(hCurrScreen, iScreenWidth, iScreenHeight);
    SelectObject(hCmpDC, hBmp);
    SetStretchBltMode(hCmpDC, COLORONCOLOR);

    // StretchBlt 是采集和缩放同时发生的关键点:
    // 源区域是完整虚拟桌面,目标区域是 iScreenWidth/iScreenHeight。
    // 防御分析:这类行为可与 GetDesktopWindow、CreateCompatibleBitmap、GetDIBits 关联。
    StretchBlt(
        hCmpDC,
        0,
        0,
        iScreenWidth,
        iScreenHeight,
        hCurrScreen,
        iScreenX,
        iScreenY,
        (int)((float)(::GetSystemMetrics(SM_CXVIRTUALSCREEN)) * fDpiRatio),
        (int)((float)(::GetSystemMetrics(SM_CYVIRTUALSCREEN)) * fDpiRatio),
        SRCCOPY);

    // 后续把 HBITMAP 转成 BMP 内存,再调用 BMP2JPEG 压缩成 JPEG。
    // 主控端收到后用 CImage 从内存流加载,所以网络包中不需要传复杂对象。
    GetDIBits(hCmpDC, hBmp, 0, (UINT)iScreenHeight, bmpSrc, (BITMAPINFO*)&hBmpInfo, DIB_RGB_COLORS);
    BMP2JPEG(szBmp, dwPicSize, jpgData);

    size = (int)jpgData->length();
    x = iScreenWidth;
    y = iScreenHeight;

    byte* data = new byte[size];
    ::memcpy(data, jpgData->c_str(), size);
    return data;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

和正常屏幕数据采集相比,屏幕墙缩略图有相同点,也有明显差异。

对比项 屏幕墙缩略图 正常屏幕插件
数据来源 当前虚拟桌面 GDI 截图 通常也是当前桌面或指定桌面/窗口
触发方式 主控定时发 COMMAND_GETMONITOR 打开某个屏幕插件后持续运行
图像尺寸 主控指定小尺寸 通常接近原始屏幕或插件自定义尺寸
状态维护 基本无状态,请求一次回一张 JPEG 可能维护差异帧、编码器状态、交互状态
回传 token TOKEN_MONITOR 各屏幕插件自己的 token/协议
是否后台桌面 第 34 课才涉及后台桌面/后台窗口
防御含义 多目标、小尺寸、周期性屏幕内容采集 单目标、更高频或更完整的屏幕内容采集

所以,屏幕墙不是“另一种后台屏幕”。它更像登录模块里内置的轻量截图接口,被主控屏幕墙定时调用,用来生成多目标缩略图。

# 8. 收到缩略图后如何显示

当被控返回缩略图数据后,主控会进入 OnReceiveComplete。它从解压缓冲区中取出图片数据,加载为 CImage,再画到列表项区域。

// 源码位置:主控\Quick\ScreenMonitorDlg.cpp:274-327
void CScreenMonitorDlg::OnReceiveComplete(ClientContext* pContext)
{
    int nCnt = m_listCtrl.GetItemCount();
    for (int i = 0; i < nCnt; i++)
    {
        ClientContext* pContext_old = (ClientContext*)m_listCtrl.GetItemData(i);
        if (pContext == pContext_old)
        {
            // 防御分析:前 5 字节是协议头/附加信息,后面才是图片数据。
            int PictureSize = pContext->m_DeCompressionBuffer.GetBufferLen() - 5;
            HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, PictureSize);
            void* pData = GlobalLock(hGlobal);
            if (!pData) return;

            memcpy(pData, pContext->m_DeCompressionBuffer.GetBuffer() + 5, PictureSize);
            GlobalUnlock(hGlobal);

            IStream* pStream = NULL;
            if (CreateStreamOnHGlobal(hGlobal, FALSE, &pStream) == S_OK)
            {
                CImage image;
                if (SUCCEEDED(image.Load(pStream)))
                {
                    CBitmap bmp;
                    HBITMAP hbmp = (HBITMAP)image.operator HBITMAP();

                    if (bmp.Attach(hbmp))
                    {
                        // 防御分析:把缩略图绘制到列表项矩形区域。
                        m_pWnd = CWnd::FromHandle(GetDlgItem(IDC_LIST)->GetSafeHwnd());
                        m_pDC = m_pWnd->GetDC();
                        m_dcimage.CreateCompatibleDC(m_pDC);
                        m_listCtrl.GetItemRect(i, &m_rect, 0);
                        m_dcimage.SelectObject(&bmp);
                        m_pDC->StretchBlt(
                            m_rect.left + 21,
                            m_rect.top,
                            m_rect.right - m_rect.left - 42,
                            m_rect.bottom - m_rect.top - 20,
                            &m_dcimage,
                            0,
                            0,
                            m_w,
                            m_h,
                            SRCCOPY);

                        // 防御分析:同时替换 ImageList 中对应缩略图,保持列表显示一致。
                        m_ImageList.Replace(i, &bmp, 0);
                    }
                }
                pStream->Release();
            }
            GlobalFree(hGlobal);
            return;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

8. 收到缩略图后如何显示(配图)

# 9. 退出监控如何清理

屏幕墙右键菜单也有“退出监控”。它会从列表中删除对应 ClientContext,清掉 MONITOR_DLG 标记,并更新图像列表和状态栏计数。

// 源码位置:主控\Quick\ScreenMonitorDlg.cpp:206-235
void CScreenMonitorDlg::DelClient(ClientContext* pContext)
{
    CCriSecLock recvlock(m_clcs);
    int nCnt = m_listCtrl.GetItemCount();
    for (int i = 0; i < nCnt; i++)
    {
        ClientContext* pContext_old = (ClientContext*)m_listCtrl.GetItemData(i);
        if (pContext == pContext_old)
        {
            // 防御分析:清掉功能占用标记,说明该连接不再属于屏幕墙。
            pContext_old->m_Dialog[1] = 0;
            m_listCtrl.DeleteItem(i);
            m_ImageList.Remove(i);
            break;
        }
    }

    // 防御分析:删除后重排 ImageList 的索引,避免缩略图错位。
    for (int i = 0; i < nCnt; i++)
    {
        LVITEM lvItem;
        lvItem.iItem = i;
        lvItem.iSubItem = 0;
        lvItem.mask = LVIF_IMAGE;
        m_listCtrl.GetItem(&lvItem);
        lvItem.iImage = i;
        m_listCtrl.SetItem(&lvItem);
    }

    CString str;
    str.Format(_T("监控:%d"), m_listCtrl.GetItemCount());
    g_pFrame->m_wndStatusBar.SetPaneText(10, str);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 10. 与单窗口屏幕功能的关系

屏幕墙和单窗口屏幕插件的关系可以这样理解:

能力 屏幕墙 单窗口屏幕插件
目标数量 多目标缩略图 单目标详细画面
主控入口 加入监控ScreenMonitorDlg 高速屏幕、差异屏幕、娱乐屏幕等菜单
刷新方式 定时发 COMMAND_GETMONITOR 插件自己的采集/编码/传输流程
主要用途 快速观察多个连接状态 深入查看和交互单个目标
本课是否讲采集原理 不讲 前面屏幕类专栏已经讲过

双击屏幕墙里的项目时,源码会打开差异屏幕,这说明屏幕墙更像入口和看板。

// 源码位置:主控\Quick\ScreenMonitorDlg.cpp:450-460
void CScreenMonitorDlg::OnDclickList(NMHDR* pNMHDR, LRESULT* pResult)
{
    POSITION pos = m_listCtrl.GetFirstSelectedItemPosition();
    int index = m_listCtrl.GetNextSelectedItem(pos);
    if (index == -1) return;

    ClientContext* pContext = (ClientContext*)m_listCtrl.GetItemData(index);
    if (pContext)
    {
        // 防御分析:双击缩略图会进入差异屏幕插件,说明屏幕墙不是详细屏幕采集实现。
        SendDll(_T("差异屏幕"));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 11. 企业检测点

层面 检测点
主控侧 多个目标被加入 ScreenMonitorDlg,状态栏 监控:N 变化
协议侧 下发 COMMAND_MONITOR 和周期性 COMMAND_GETMONITOR
网络侧 多个被控周期性返回小尺寸图片数据
被控侧 GetDesktopWindowCreateCompatibleBitmapStretchBltGetDIBits、JPEG 转换短时间内连续出现
隐私侧 即使是缩略图,也属于屏幕内容采集,必须有授权和可见提示
日志侧 关注“加入监控”“退出监控”“使用差异屏幕”等日志事件

# 12. 合法练习题

  1. 只读源码,画出 QuickView.cpp -> COMMAND_MONITOR -> MainFrm.cpp -> ScreenMonitorDlg 的链路。
  2. 根据 ScreenMonitorDlg::OnTimer,说明屏幕墙请求包由哪些字段组成。
  3. 根据 OnReceiveComplete,说明缩略图数据从解压缓冲区到列表显示经历了哪些对象。
  4. 设计一条合规要求:屏幕墙监控多个终端时,必须展示哪些授权和审计信息。
  5. 对比 COMMAND_GETMONITOR 和单窗口屏幕插件,说明为什么屏幕墙缩略图是轻量快照而不是完整远程屏幕会话。

# 13. 本课小结

屏幕墙是主控侧的多目标缩略图看板。它通过“加入监控”下发 COMMAND_MONITOR,收到 TOKEN_MONITOR 后把目标加入 ScreenMonitorDlg,再由定时器周期性发送 COMMAND_GETMONITOR 请求缩略图。缩略图在被控登录模块里通过 GetScreen 抓取当前虚拟桌面、缩放、转 JPEG 后回传,主控收到后绘制到列表项中。它不是后台屏幕或后台桌面实现的一部分,也不是差异屏幕那套增量采集链路,不能把这些能力混为一谈。

安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。

上次更新: 2026/07/14, 19:37:40

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>