- (WTSVirtualChannelManager*)vcm->quque。用于发送。发送时都先放入这个queue。系统中只有一个。静态、动态虚似通道都用这个queue。
- (rdpPeerChannel*)channel->queue。用于接收,接收时会根据channelId分投到这些queue。系统中有多少个通道,就有多少个queue。
- 虚拟通道分静态虚拟通道和动态虚拟通道。动态虚拟通道是指在静态通道上使用动态通道扩展,它含了两个概念:首先它是静态通道,其次是在传输数据上用了动态通道扩展(EDYC: Dynamic Channel Virtual Channel Extension)
一个rdpPeerChannel封装一条收发pdu的通道,一般指虚似通道(virtual channel),一个wMessageQueue封该通道收发的消息,一个wMessage则封装一个消息。
一、wMessageQueue结构

建议结合MessageQueue_Dispatch代码,来理解图1。
struct _wMessageQueue { int head; int tail; int size; int capacity; wMessage* array; CRITICAL_SECTION lock; HANDLE event; // 新放入一个wMessage后,便会置它有信号 wObject object; }; typedef struct _wMessageQueue wMessageQueue;
- head、tail。用于指示头、尾。会发生绕转,即tail值小于head。范围[0, capacity / sizeof(wMessage) - 1]。示例:head值是2,tail则是6。
- size。队列中有效的wMessage个数。示例;size值是4。
- capacity。内存块占用的字节数。示例:capacity值是7*sizeof(wMessage)。
- event:一旦有新wMessage放入队列,会置这个事件有信号。此时,系统其它地方会有一个线程在检查这事件是否有信号,发现有信号,于是被唤醒,处理这个wMessage。是要发送的,就发送;是接收的,继续后绪处理。
这些wMessage以数组结构放在queue->array,释放内存块方法是free(queue->array),即所有wMessage一并释放,禁止类似free(queue->array[i])方法单独释放一个wMessage。
二、MessageQueue_Clear和fnObjectFree
struct _wMessage { UINT32 id; void* context; void* wParam; void* lParam; UINT64 time; MESSAGE_FREE_FN Free; }; typedef struct _wMessage wMessage; int MessageQueue_Clear(wMessageQueue* queue) { int status = 0; EnterCriticalSection(&queue->lock); while (queue->size > 0) { wMessage* msg = &(queue->array[queue->head]); /* Free resources of message. */ if (queue->object.fnObjectUninit) queue->object.fnObjectUninit(msg); if (queue->object.fnObjectFree) queue->object.fnObjectFree(msg); ZeroMemory(msg, sizeof(wMessage)); queue->head = (queue->head + 1) % queue->capacity; queue->size--; } ResetEvent(queue->event); LeaveCriticalSection(&queue->lock); return status; }
MessageQueue_Clear用于清空queue。注意,不是释放,释放用的是MessageQueue_Free,MessageQueue_Free首先调用MessageQueue_Clear清空queue,然后调用free(queue->array)。对MessageQueue_Clear,枚举出队列中有效wMessage,针对每个wMessage,依次调用fnObjectUninit、fnObjectFree。通常fnObjectUninit是nulllptr,但fnObjectFree一般不是nullptr,它要释放挂着在该wMessage上的内存。
“禁止类似free(queue->array[i])方法单独释放一个wMessage”,那wMessage还要释放什么内存?——看下struct wMessage定义,既然一个wMessage表示一个消息,存储消息需要一个“内存块”,这“内存块”是放在wMessage哪个字段。以下是两种方案。
- “内存块”就是一真的内存块。地址放在wParam,字节数放在lParam。wts_virtual_channel_manager_free_message实现如何释放这种情况下挂接的内存。
- “内存块”定义为struct,像wtsChannelMessage。wMessage.context存储着wtsChannelMessage地址,wtsChannelMessage.length则指示内存块字节数。wts_wtsChannelMessage_free_message实现如何释放这种情况下挂接的内存。
既然存在多种把“内存块”挂接到wMessage的方法,具体用什么方法只有该wMessageQueue的使用者才能知道,queue->object.fnObjectFree功能就是让释放挂接到该wMessage的内存,而不是释放wMessage本占占用的那块内存。
static void wts_virtual_channel_manager_free_message(void* obj) { wMessage* msg = (wMessage*)obj; if (msg) { BYTE* buffer = (BYTE*)msg->wParam; if (buffer) free(buffer); } }
三、虚拟通道(Virtual Channels)
虚拟通道分为静态虚拟通道(Static Virtual Channels)和动态虚拟通道(Dynamic Virtual Channels)。
pdu总的来说分两类,tpkt pdu和fast-path pdu。不论静态,还是动态虚拟通道,都使用tpdk pdu。
fast-path pdu分fast-path input event pdu和fast-path update pdu,对client来说,不会有Server向它发input事件,因而只会收到指示图像更新的fast-path update pdu。对使用RemoteFx技术来说,收发图像数据通过fast-path update pdu。但使用gfx技术时,收发图像不是通过fast-path update pdu,而是动态虚拟通道。
动态虚拟通道是指在静态通道上使用动态通道扩展,它含了两个概念:首先它是静态通道,其次是在传输数据上用了动态通道扩展(EDYC: Dynamic Channel Virtual Channel Extension)。看FreeRDP_WTSVirtualChannelWrite,可很快了解区别。
BOOL WINAPI FreeRDP_WTSVirtualChannelWrite(HANDLE hChannelHandle, PCHAR Buffer, ULONG Length, PULONG pBytesWritten) { ... if (channel->channelType == RDP_PEER_CHANNEL_TYPE_SVC) { length = Length; buffer = (BYTE*)malloc(length); // 静态虚拟通道。 // 要发送的负载不作任处理,就发向网络。 CopyMemory(buffer, Buffer, length); totalWritten = Length; ret = wts_queue_send_item(channel, buffer, length); } else if (!channel->vcm->drdynvc_channel || (channel->vcm->drdynvc_state != DRDYNVC_STATE_READY)) { DEBUG_DVC("drdynvc not ready"); return FALSE; } else { // 动态虚拟通道。 // 要发送的负载要按[MS-RDPEDYC]规范进行分子包,像一个子包最多1600字节, // 每个子包前面还要加数个字节的头,然后再发向网络。 first = TRUE; while (Length > 0) { // VirtualChannelChunkSize: 1600 s = Stream_New(NULL, channel->client->settings->VirtualChannelChunkSize); ... buffer = Stream_Buffer(s); Stream_Seek_UINT8(s); cbChId = wts_write_variable_uint(s, channel->channelId); if (first && (Length > (UINT32)Stream_GetRemainingLength(s))) { cbLen = wts_write_variable_uint(s, Length); buffer[0] = (DATA_FIRST_PDU << 4) | (cbLen << 2) | cbChId; } else { buffer[0] = (DATA_PDU << 4) | cbChId; } first = FALSE; written = Stream_GetRemainingLength(s); if (written > Length) written = Length; Stream_Write(s, Buffer, written); length = Stream_GetPosition(s); Stream_Free(s, FALSE); Length -= written; Buffer += written; totalWritten += written; ret = wts_queue_send_item(channel->vcm->drdynvc_channel, buffer, length); } } ... }
- 静态虚拟通道。要发送的负载不作任处理,就发向网络。
- 动态虚拟通道。要发送的负载要按[MS-RDPEDYC]规范进行分子包,像一个子包最多1600字节,每个子包要加数个字节的头,然后再发向网络。
以下为方便说明,用的是一实例,存在4个虚拟通道。

rdpdr(File System Virtual Channel Extension)、drdynvc都在用动态通道扩展。
四、动态虚拟通道:drdynvc_channel、rdpgfx_channel
使用gfx时,server通过动态虚拟通道把图像发到client,通道name固定是“drdynvc”。
drdynvc_channel、rdpgfx_channel类型都是rdpPeerChannel,它们都和gfx发送图像有关。
- drdynvc_channel。调用FreeRDP_WTSVirtualChannelOpen创建,归属WTSVirtualChannelManager* vcm,mcs->channels[3].handle也指向它。channelType: RDP_PEER_CHANNEL_TYPE_SVC(0),channelId: 1007,index: 3。
- rdpgfx_channel。调用FreeRDP_WTSVirtualChannelOpenEx创建,归属RdpgfxServerPrivate* priv。channelType: RDP_PEER_CHANNEL_TYPE_DVC(1),channelId: 2,index: 0,这里的channelId和mcs中的通道编号没关系,freerdp会把dvc统一放在一个vcm->dynamicVirtualChannels数组,工具函数wts_get_dvc_channel_by_id会根据这个channelId找到这条dvc。
总的来说,drdynvc_channel是个静态通道(SVC),rdpgfx_channel是个动态通道(DVC)。动态通道是建立在静态通道上的,server和client协商一致,认为在“drdynvc”这个静态通道上,用动态通道扩展更有利于实现gfx。于是在先后次序上,是先有drdynvc_channel,再有rdpgfx_channel。
4.1 双方协商建立通道
1. 在连接阶段,client发现此次会话有需要用drdynvc传输的数据,像图像(rdpgfx),于是发MCS Connect Initial PDU with GCC Conference Create Request时,在clientNetData部分告知server,须要创建一个名称是“drdynvc”的通道。
2. server回给client的MCS Connect Response PDU with GCC Conference Create Response中,告知“drdynvc”的通道号,像1007。
3. connection sequence结束,双方进入DVC Setup(MS-RDPEDYC.pdf 1.3.2 DVC Setup)。建立DVC通道分两个步骤,server向client发DVC Capabilities Request PDU,client向server回DVC Capabilities Response PDU,这两个pdu的Cmd都是0x05。
3.1 server端。freerdp在哪里发出DVC Capabilities Request PDU?——握手成功后,server的rdpd_slice会检查client->activated,一旦发现是true(意味着已完成connection sequence),就会发DVC Capabilities Request PDU。参考WTSVirtualChannelManagerCheckFileDescriptor中的“dynvc_caps = 0x00010050”,这UINT32值定义了cbId=0、Sp=0、Cmd=0x05、Pad=1、Version=0x0001。
3.2 client端。client收到DVC Capabilities Request PDU后,需在恰当时机处理。为什么要突出“恰当时机”?——client收到connection sequence最后一个应答和DVC Capabilities Request PDU可能是同一次read操作,而要能处理Capabilities Request PDU,client除需要已创建处理“drdynvc”通道的专门线程(drdynvc_virtual_channel_client_thread)外,还需让CHANNEL_CLIENT_DATA中指针函数pChannelOpenEventProc/pChannelOpenEventProcEx至少有一个是有效值(“drdynvc”用的是pChannelOpenEventProcEx,指向drdynvc_virtual_channel_open_event_ex,会执行把此个pdu转成wMessage放入drdynvc->queue队列),freerdp_channels_post_connect执行这个赋值操作。所以处理这次“合成read”可分为三步,1)处理connection sequence最后个response,让rdp->state = CONNECTION_STATE_ACTIVE。2)执行freerdp_channels_post_connect。3)处理DVC Capabilities Request PDU,向server发DVC Capabilities Response PDU。这三步骤参考HandleReadResult。
4. 不管“drdynvc”通道要传输多少种数据,DVC Capabilities Request PDU、DVC Capabilities Response PDU只须要一对。但后面的CREATE_REQUEST_PDU就是几种数据就要几个。
4.2 server发送图像数据
参考“RDP(4/5):Server处理图像”中的“rdpgfx_send_surface_frame_command”。
4.3 server接收client发来的pdu
server用drdynvc通道向client发图像数据,client也会用它向server发来pdu,当中肯定会发的两种。
- RDPGFX_CAPS_ADVERTISE_PDU(0x0012)。The RDPGFX_CAPS_ADVERTISE_PDU message is sent by the client to advertise supported capabilities。在没发图像前就会收到,只一个。
- RDPGFX_FRAME_ACKNOWLEDGE_PDU(0x000D)。client每收到一视频帧,就会向server发一个。当中有个frameId字段,指示这个确认针对的哪一帧。因为frameId是连续的,server可根据收到的frameId判断外面还“挂着”多少帧。
收到一个pdu后,MessageQueue_Dispatch把该pdu相关wMessage放入rdpgfx_channel->queue。那是谁进一步处理收到的wMessage?——rdpgfx_server_thread_func。
<freerdp>/channels/rdpgfx/server/rdpgfx_main.c ------ static DWORD WINAPI rdpgfx_server_thread_func(LPVOID arg) { RdpgfxServerContext* context = (RdpgfxServerContext*)arg; RdpgfxServerPrivate* priv = context->priv; DWORD status; DWORD nCount; void* buffer; HANDLE events[8]; UINT error = CHANNEL_RC_OK; buffer = NULL; nCount = 0; events[nCount++] = priv->stopEvent; // priv->channelEvent是priv->rdpgfx_channel->queue->event原样复制。 events[nCount++] = priv->channelEvent; /* Main virtual channel loop. RDPGFX do not need version negotiation */ while (TRUE) { status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE); ... /* Stop Event */ if (status == WAIT_OBJECT_0) break; if ((error = rdpgfx_server_handle_messages(context))) { WLog_ERR(TAG, "rdpgfx_server_handle_messages failed with error %" PRIu32 "", error); break; } } ... }
priv->channelEvent是priv->rdpgfx_channel->queue->event原样复制,后者有信号,就是它有信号。知道有来自client的新wMessager后,rdpgfx_server_handle_messages按[MS-RDPEDYC]规范装配出pdu,调用rdpgfx_server_receive_pdu处理每个pdu。
五、静态虚拟通道:cliprdr
rdp在实现剪贴板时使用了静态虚拟通道,该通道名称:cliprdr。
以下是剪贴板时的rdpPeerChannel,以及发送时的wMessage。
struct rdpPeerChannel { WTSVirtualChannelManager* vcm; // channel_new直接传入。外部对象,释放时不要释放。 freerdp_peer* client; // channel_new直接传入。外部对象,释放时不要释放。 void* extra; // nullptr UINT16 index; // channel_new直接传入, 指示此个channel在mcs->channels数组中索引。 对server来说,mcs->channels在连接阶段收到client发来的Client MCS Connect Initial PDU with GCC Conference Create Request时被初始化。参考(gcc.c)gcc_read_client_network_data。 UINT32 channelId; // 1005,分配给该通道的通道号 UINT16 channelType; // RDP_PEER_CHANNEL_TYPE_SVC(0),指示此个是静态虚拟通道 UINT32 channelFlags; // wStream* receiveData; // channel_new时用Stream_New(NULL, chunkSize)创建 wMessageQueue* queue; // channel_new时用MessageQueue_New(&queueCallbacks)创建。queueCallbacks类型是wObject,它有一个成员fnObjectFree,是个函数指针,指向wts_wtsChannelMessage_free_message。 BYTE dvc_open_state; UINT32 dvc_total_length; rdpMcsChannel* mcsChannel; }; struct wMessage { UINT32 id; // 其它是type,固定填0 void* context; // 其实是channelId,1006 void* wParam; // buffer指针 void* lParam; // buffer长度 UINT64 time; MESSAGE_FREE_FN Free; // nullptr };
cliprdr生成的wMessage是放入哪个wMessageQueue?FreeRDP_WTSVirtualChannelWrite用的是channel->vcm->queue,而channel_new创建的是rdpPeerChannel.queue,vcm是管理众多channel的,它难道是把所有wMessage放到同一个queue?——是的。要发送时,用的是channel->vcm->queue,也就是说,所有channel发送时用的是同一个queue。channel->queue则用于接收时,rdpd thread收到一个channel pdu时,会根据channelId投放到对应channel的channel->queue。
在发送时。会调用WTSVirtualChannelManagerGetEventHandle(client->vcm),它返回vcm->queue的event。一旦该事件有信号,就会把vcm->queue中数据发向网络。
在接收时。有一个新的wMessage放入channel->queue后,会把channel->queue->event置为有信号。但cliprdr_server_thread等待的是CliprdrServerPrivate.ChannelEvent,ChannelEvent是个什么事件?——它们是同一个值,通过WTSVirtualChannelQuery(cliprdr->ChannelHandle, WTSVirtualEventHandle, &buffer, &BytesReturned),ChannelEvent就是queue->event的复制品。
rdpd thread收到cliprdr pdu,到cliprdr-recv-thread读取,这当前事件所有内存是怎么处理的?
- [RdpdThread]收到的数据放在rdpPeerChannel.receiveData这个wSteram中(WTSProcessChannelData())。
- [RdpdThread](wts_queue_receive_data())生成一个wtsChannelMessage结构,注意这里的大小是sizeof(wtsChannelMessage) + Length。为什么要加Length,Length是此次收到的channel数据长度,它将紧挨着wtsChannelMessage最后一个字节存放,这样释放时一次free就可以。于是数据就复制到了新生成的“wtsChannelMessage”结构中,wMessage.context指向“wtsChannelMessage”指针。wtsChannelMessage.legnth存储着字节数,因而不须要wMessage有专门字段存储字节数。——到此数据已被放入channel->queue。
- [cliprdr_server_thread]发现channel->queue有信号后,调用WTSVirtualChannelRead,数据是从CliprdrServerPrivate.s这个wStream中取的,怎么回事?——一个完整PDU,像CB_CLIP_CAPS、CB_FORMAT_LIST_RESPONSE,它们可能被拆到多个virtual pdu,这时要把这些pdu“合并”,传给后面的是一个完整的pud,像cliprdr_server_receive_pdu只处理一个完整pdu,CliprdrServerPrivate.s就用于处理这个“合并”过程,并存放着“合并”结果。
六:suppressOutput、activated、gfxOpened、GfxH264
rdpShadowClient.suppressOutput。是否要抑制输出。初始false。PDUTYPE2_SUPPRESS_OUTPUT(0x23),freerdp叫DATA_PDU_TYPE_SUPPRESS_OUTPUT,Suppress Output PDU Data可修改这个字段。什么时候发生呢?客户端认为client没必要发屏幕图像时,像界面最小化了。即在运行过程中,这值会false、true之间变。
如何使用suppressOutput。
- suppressOutput = false时。1)采到新帧,而且距离“第一帧”至少8秒或以上(这个8秒的作用是让确保该帧已被写入写缓冲),2)qos一直没变、时间超过8秒,==》要断开。原因。1)不要希望每发出一帧,Client就会回一个qos。为安全,设为5帧。
- 从开始很久了与没可以创建抓拍线程,此时要断开连接。
- 对30后钟后,自动断开连接。
- void wfClipboard_kos::hdrop_copied(const std::vector<std::string>& files),相同时还是要继续发。
rdpShadowClient.activated。该client的连接阶段是否已经成功连接。是true后,表示server可以向这client发屏幕数据了。
pgfxstatus->gfxOpened。是否已经打开了RDPGFX dynamic channel。初始是false。收到client通过“drdynvc”通道发来的DVC Capabilities Response PDU后设为true。只有设为true后,server才可以向这client发屏幕数据。
context->settings->GfxH264。赋值线程:rdpgfx_server_thread_func。该线程要等pgfxstatus->gfxOpened=true时创建。
这三个变量的赋值时间,先activated,后gfxOpened,最后GfxH264。
七、一些补充
(gcc.c)gcc_write_client_network_data。client向server发Client MCS Connect Initial PDU with GCC Conference Create Request,填写当中含有虚拟通道的CS_NET。
nego_connect调用nego_security_connect。依据client返回的nego->SelectedProtocol,nego_security_connect会采取不同动作。
是PROTOCOL_HYBRID时,执行transport_connect_nla。该函数1)进行tls握手,2)进入CONNECTION_STATE_NLA状态,开始CredSSP协商。
是PROTOCOL_SSL时,执行transport_connect_tls。该函数执行tls握手,而且不改变rdpRdp.state,即nego_connect完成后还是CONNECTION_STATE_NEGO。nego_connect的上层函数rdp_client_connect发现此时rdpRdp.state不是CONNECTION_STATE_NLA,于是调用mcs_client_begin(如果在nla,这操作是在transport_connect_nla执行),开始发送第一个需要加密发送的pdu:MCS Connect Initial PDU with GCC Conference Create Request。