【UE C++】WebSockets接收过长BinaryMessage时分段问题的解决方案 需改UE源码
问题简介
基于我上一篇文章 《虚幻引擎WebSocket简单应用》
在实际项目使用中,发现在大数据量,间隔时间短的情况下 UE接收到的数据被分段处理。
本文通过修改UE websockets模块源码
的方式,解决此问题。
tips: 需要去Github 上Epic ue获取引擎源码 修改编译。
问题复现
通过在TestWebsockets.cpp文件 OnPacketReceived方法打断点调试发现
从底层传来的 Data 被分段, Size 最大为8192 ,最可怕的是BytesRemaining(分段剩余字节数)一直为0,导致我们无法在此方法中把完整的消息组装起来,也无法判断消息片段处于哪一种状态。
疑似问题原因
WS Server端压缩
参考文章 链接: WebSocket的压缩引发的问题 发现在实际应用场景中,大多Websocket Server会通过Nginx代理,在其实现端(Java等后端)和服务器端会有一些配置 来保障WebSocket建立多连接及大数据量场景下的性能,其中压缩协议是很常见的Sec-WebSocket-Extensions: permessage-deflate
也许通过某些后端配置也可以解决分段问题,但笔者与公司后端排查后未解决,如有相关配置欢迎评论区讨论。
C++ LibWebSockets库
LibWebSockets通过官网学习:官网
lws_remaining_packet_payload’ doesn’t work with ‘permessage_deflate’ enabled
引用回答
That api tells you what’s left in the fragment, which is all ws protocol tells us (NOT the message size).
With pmd, the message and the fragment contain compressed data. It’s not meaningful to tell you how much compressed data remains in the fragment, since this has an unknown relationship to the amount of decompressed data it will deflate to.
Instead of looking for information that doesn’t exist, lws can tell you what info it does have via lws_is_final_fragment()… it knows if it’s on the final fragment of the compressed message. And it knows if it is in the middle of passing the deflated data from that buffer to you. So it can delay telling you it’s final until it is on the last chunk of the final compressed bufferload. That will be correct then both for compressed and uncompressed messages.
from libwebsockets.
UE的一个TODO
在UE源码Websocket模块中 LwsWebSocketsManager.cpp 148行
// TODO: Investigate why enabling LwsExtensions prevents us from receiving packets larger than 1023 bytes, and also why lws_remaining_packet_payload returns 0 in that case
调查为什么启用LwsExtensions会阻止我们接收大于1023字节的数据包,以及为什么在这种情况下lws_remaining_packet_payload返回0
去Github上看了下历代版本UE源码 (4.21 ~ 5.3 截止2023年10月)
这个TODO是一直都在 看来还得靠自己啊 O(∩_∩)O
自己解决问题
主要修改了两个文件 WebSockets模块中
- LwsWebSocketsManager.cpp
- LwsWebSocket.cpp
在LwsWebSocketsManager中InitWebSockets是对lws的应用,包括对lws对象的一些配置,及注册ws相关回调方法。在86行 LwsProtocol.callback = &FLwsWebSocketsManager::StaticCallbackWrapper;
是一个关键方法的回调注册
所有的回调通过 LwsWebSocket.cpp文件LwsCallback 处理 Socket->LwsCallback(Connection, Reason, Data, Length);
在LwsWebSocket.cpp是lws根据WS状态(协议)来处理收到的消息的具体实现,在接收Binary Message时会根据Remain (BytesLeft)字段 判断是否已经完整接收一段消息,并把消息推送到消息队列
case LWS_CALLBACK_CLIENT_RECEIVE:
const SIZE_T BytesLeft = lws_remaining_packet_payload(Instance);
if (BytesLeft == 0){
bWakeGameThread = true;
ReceiveTextQueue.Enqueue(MakeUnique<FLwsReceiveBufferText>(MoveTemp(ReceiveBuffer)));
ReceiveBuffer.Empty();}
正如UE源码这个TODO所说: 自己调查为啥lws_remaining_packet_payload方法总是返回0 也就是所有被分段的信息都被标记为完成接收状态并由消息队列广播出去。
修改WS缓冲区大小
未处理完成的消息会被存入缓冲区 既然已经到了修改源码的层面,我们可以把缓冲区的大小调大一些
LwsWebSocketsManager.cpp
for (const FString& Protocol : Protocols)
{
FTCHARToUTF8 ConvertName(*Protocol);
// We need to hold on to the converted strings
ANSICHAR* Converted = static_cast<ANSICHAR*>(FMemory::Malloc(ConvertName.Length() + 1));
FCStringAnsi::Strcpy(Converted, ConvertName.Length(), (const ANSICHAR*)ConvertName.Get());
lws_protocols LwsProtocol;
FMemory::Memset(&LwsProtocol, 0, sizeof(LwsProtocol));
LwsProtocol.name = Converted;
LwsProtocol.callback = &FLwsWebSocketsManager::StaticCallbackWrapper;
LwsProtocol.per_session_data_size = 0; // libwebsockets has two methods of specifying userdata that is used in callbacks
// we can set it ourselves (during lws_client_connect_via_info - we do this, or via lws_set_wsi_user)
// or libwebsockets can allocate memory for us using this parameter. We want to set it ourself, so set this to 0.
LwsProtocol.rx_buffer_size = 10 * 1024 * 1024; // Largest frame size we support
LwsProtocol.tx_packet_size = 10 * 1024 * 1024;
LwsProtocols.Emplace(MoveTemp(LwsProtocol));
}
// LWS requires a zero terminator (we don't pass the length)
LwsProtocols.Add({ nullptr, nullptr, 0, 0 });
// Subscribe to log events. Everything except LLL_PARSER
static_assert(LLL_COUNT == 11, "If LLL_COUNT increases, libwebsockets has added new log categories, analyze if we should be listening to them");
lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO | LLL_DEBUG | LLL_HEADER | LLL_EXT | LLL_CLIENT | LLL_LATENCY | LLL_USER, &LwsLog);
struct lws_context_creation_info ContextInfo = {};
ContextInfo.port = CONTEXT_PORT_NO_LISTEN;
ContextInfo.protocols = LwsProtocols.GetData();
ContextInfo.uid = -1;
ContextInfo.gid = -1;
ContextInfo.options |= LWS_SERVER_OPTION_PEER_CERT_NOT_REQUIRED | LWS_SERVER_OPTION_DISABLE_OS_CA_CERTS | LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
ContextInfo.max_http_header_data = 0;
int32 MaxHttpHeaderData = 1024 * 32;
GConfig->GetInt(TEXT("WebSockets.LibWebSockets"), TEXT("MaxHttpHeaderData"), MaxHttpHeaderData, GEngineIni);
ContextInfo.max_http_header_data2 = MaxHttpHeaderData;
ContextInfo.pt_serv_buf_size = MaxHttpHeaderData;
修改lws处理消息的回调方法
LwsWebSocket.cpp
通过阅读libweosocket库官网,仔细阅读了每一个处理消息的方法
发现除了lws_remaining_packet_payload来判断 剩余多少分段消息外
还可以用lws两个api来判断 lws_is_first_fragment lws_is_final_fragment
废话不多说贴代码
case LWS_CALLBACK_CLIENT_RECEIVE:
{
isFirst = lws_is_first_fragment(Instance);
isEnd = lws_is_final_fragment(Instance);
BytesLeft = lws_remaining_packet_payload(Instance);
UE_LOG(LogWebSockets, VeryVerbose, TEXT("FLwsWebSocket[%d]::LwsCallback: Received LWS_CALLBACK_CLIENT_RECEIVE Length=%d BytesLeft=%d"), Identifier, Length, BytesLeft);
bool bWakeGameThread = false;
if (bWantsMessageEvents)
{
FUTF8ToTCHAR Convert((const ANSICHAR*)Data, Length);
ReceiveBuffer.Append(Convert.Get(), Convert.Length());
if (isEnd == 1)
{
bWakeGameThread = true;
ReceiveTextQueue.Enqueue(MakeUnique<FLwsReceiveBufferText>(MoveTemp(ReceiveBuffer)));
ReceiveBuffer.Empty();
}
}
if (bWantsRawMessageEvents)
{
bWakeGameThread = true;
ReceiveBinaryQueue.Enqueue(MakeUnique<FLwsReceiveBufferBinary>(static_cast<const uint8*>(Data), Length, BytesLeft));
}