解决Unreal Engine使用third party libwebsockets无法连接问题

news/2025/2/22 14:40:00/文章来源:https://www.cnblogs.com/lenomirei/p/18725049

背景

使用libwebsockets开发了一个SDK,用于建立和服务器的连接,并就接受服务器的推送消息,使用的版本是4.3.3的tag。UE版本是5.3.2
以动态库的方式接入整体SDK,SDK链接了静态的libwebsockets,在qt demo运行良好,但是在接入Unreal Engine demo的时候出现问题。

使用的IDE为Visual Studio 2019,工具集为v142,Windows SDK版本为10.0.19041.0

问题

在接入UE demo的时候,发现在lws_client_connect_via_info调用后紧接着调用lws_service时出现连接失败的情况,也无法收到连接错误的LWS_CALLBACK_CLIENT_CONNECTION_ERROR的回调,只能等待本地定的超时timer超时终止连接。

其中lws_client_connect_via_info主要用于提供连接参数并处理连接,lws_service用来跑lws的消息循环来触发回调。

定位

首先在单步调试过程中偶然发现lws_client_connect_via_info调用后紧接着调用lws_service的连续调用如果在中间插入一个休眠时间(500ms),连接就能成功建立。首先能确定的是,并没有多线程引发问题,所有lws相关的调用均在同一个线程内完成,能确保调用顺序是lws_client_connect_via_info调用后再调用lws_service。sample中也是这么调用的,按理说不应该出现问题。

打开了lws的日志开关,发现有这么一句报错lws_client_connect_check: errno 10022,在这句报错后,到超时前没有其他错误日志,顺着这个报错找到如下对应的源码,

connect的时候报错10022,表示参数错误,但是通过调试发现参数并无错误,这里报错会导致后续流程无法进行,符合日志显示情况,针对这里做一些猜想和认证

static lcccr_t
lws_client_connect_check(struct lws *wsi, int *real_errno)
{int en = 0;
#if !defined(WIN32)int e;socklen_t sl = sizeof(e);
#endif(void)en;/** This resets SO_ERROR after reading it.  If there's an error* condition, the connect definitively failed.*/...if (!connect(wsi->desc.sockfd, (const struct sockaddr *)&wsi->sa46_peer.sa4,
#if defined(WIN32)sizeof(struct sockaddr)))
#else0))
#endifreturn LCCCR_CONNECTED;en = LWS_ERRNO;if (en == WSAEISCONN) /* already connected */return LCCCR_CONNECTED;if (en == WSAEALREADY) {/* reset the POLLOUT wait */if (lws_change_pollfd(wsi, 0, LWS_POLLOUT))lwsl_wsi_notice(wsi, "pollfd failed");} // 这里的代码会影响后续执行流程,当en为10022时,pollfd不会被更改,导致后续流程无法进行(无法循环检查连接是否成功)if (!en || en == WSAEINVAL ||en == WSAEWOULDBLOCK ||en == WSAEALREADY) {lwsl_wsi_debug(wsi, "errno %d", en);return LCCCR_CONTINUE;} // 这里10022时返回继续,意图是继续检查流程,配合上面的pollfd来实现循环多次检查
#endiflwsl_wsi_notice(wsi, "connect check FAILED: %d",*real_errno || en);return LCCCR_FAILED;
}

这个函数的调用是在lws_client_connect_via_info第一次connect连接之后,也就是在lws_service中检测连接的情况来确认是否进行后续调用(TLS连接)来完成websocket连接的。这里仅在UE demo上出问题是十分费解的,在其他场景下测试,这里的错误码是10037,表示socket正在处理中,作者的想法可能是希望这里循环检查,直到连接成功后进行后续步骤,如果还没准备好就是10037,返回LCCCR_CONTINUE支持后续检查。

可按理说WSAEINVAL也会返回LCCCR_CONTINUE来确保继续运行,但是日志显示又一次10022后check就不再工作了,但是lws_service一直在调用,这里表现非常奇怪,如果打断点调试,就能正确触发这里的循环检查(和休眠逻辑类似),感觉这里有些奇怪。

验证


用wireshark抓包发现,TCP连接其实已经建立成功了,但是后续都没有执行(预期后续建立TLS连接,这个过程可能是在lws_service中进行),强制修改忽视第二次connect的错误继续执行,就正常建立连接,可以接收消息了,原因就出在10022错误码导致的流程中断,这个中断可能是正常的,因为真正的参数错误不应该继续连接。

我修改了如下测试代码,替换了dll的主功能,来测试第二次connect的错误码情况,在其他场景下使用该dll时(控制台程序,qt demo unity demo)第二次connect时返回的错误码基本都是10037,代表对应的socket已经在处理流程中(因为是非阻塞连接,需要等待),10037错误码是正常表现,但是在接入UE demo后,情况不同了,第二次connect返回的错误码是10022,到这里基本能确定其实与lws的业务逻辑无关,仅是因为两次connect在UE中的表现确实和其他场景不符,不太确定UE是否对网络连接做了额外的动作。

WSADATA wsaData;SOCKET sock = INVALID_SOCKET;if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {return 1;}sock = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (sock == INVALID_SOCKET) {::WSACleanup();return 1;}// 设置非阻塞u_long mode = 1;if (::ioctlsocket(sock, FIONBIO, &mode) == SOCKET_ERROR) {::closesocket(sock);::WSACleanup();return 1;}sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(443);::inet_pton(AF_INET, "your ip address", &serverAddr.sin_addr);int error_test = 0;socklen_t len = sizeof(error_test);::getsockopt(sock, SOL_SOCKET, SO_ERROR, (char*)&error_test, &len);common::HGLogger::warn("SO_ERROR : {}", error_test);// 第一次 connectint result = ::connect(sock, (sockaddr*)&serverAddr, sizeof(serverAddr));if (result == SOCKET_ERROR) {int error = ::WSAGetLastError();common::HGLogger::warn("error : {}", error);if (error == WSAEWOULDBLOCK) {}else {}}::getsockopt(sock, SOL_SOCKET, SO_ERROR, (char*)&error_test, &len);common::HGLogger::warn("SO_ERROR : {}", error_test);// 第二次 connectresult = ::connect(sock, (sockaddr*)&serverAddr, sizeof(serverAddr));if (result == SOCKET_ERROR) {int error = ::WSAGetLastError();common::HGLogger::warn("error : {}", error);if (error == WSAEISCONN) {}else {}}::getsockopt(sock, SOL_SOCKET, SO_ERROR, (char*)&error_test, &len);common::HGLogger::warn("SO_ERROR : {}", error_test);::closesocket(sock);::WSACleanup();

10022的问题确认之后,看一下lws流程中断的问题,由于lws认为是参数错误,所以pollfd不会被修改,导致后续不再循环检查(所以sleep一段时间确保第一次连接成功后就能完成整个流程,是因为到这里检查直接显示已经连接),进行如下代码,强制在参数错误时也继续流程。

if (en == WSAEALREADY || en == WSAEINVAL) {/* reset the POLLOUT wait */if (lws_change_pollfd(wsi, 0, LWS_POLLOUT))lwsl_wsi_notice(wsi, "pollfd failed");} // 这里的代码会影响后续执行流程,当en为10022时,pollfd不会被更改,导致后续流程无法进行(无法循环检查连接是否成功)

就正常了,可以继续流程,总得来说就是这两个场景导致的问题。

修改方案

修改验证的逻辑,将多次调用的connect更换为select,同时保留循环检查的逻辑,给select设置一个较短的超时时间。避开10022错误,因为强制参数错误继续不合理,所以看看select能否避开这个错误并继续流程。

static lcccr_t
lws_client_connect_check(struct lws *wsi, int *real_errno)
{int en = 0;
#if !defined(WIN32)int e;socklen_t sl = sizeof(e);
#endif(void)en;/** This resets SO_ERROR after reading it.  If there's an error* condition, the connect definitively failed.*/#if !defined(WIN32)if (!getsockopt(wsi->desc.sockfd, SOL_SOCKET, SO_ERROR, &e, &sl)) {en = LWS_ERRNO;if (!e) {lwsl_wsi_debug(wsi, "getsockopt: conn OK errno %d", en);return LCCCR_CONNECTED;}lwsl_wsi_notice(wsi, "getsockopt fd %d says e %d",wsi->desc.sockfd, e);*real_errno = e;return LCCCR_FAILED;}#elsefd_set write_set, except_set;struct timeval tv;int ret;FD_ZERO(&write_set);FD_ZERO(&except_set);FD_SET(wsi->desc.sockfd, &write_set);FD_SET(wsi->desc.sockfd, &except_set);tv.tv_sec = 0;tv.tv_usec = 1;ret = select((int)wsi->desc.sockfd + 1, NULL, &write_set, &except_set, &tv);if (ret > 0 && FD_ISSET(wsi->desc.sockfd, &write_set)) {lwsl_wsi_debug(wsi, "select write fd set, conn OK");return LCCCR_CONNECTED;}if (!ret) {if (lws_change_pollfd(wsi, 0, LWS_POLLOUT))lwsl_wsi_notice(wsi, "pollfd failed");lwsl_wsi_debug(wsi, "select timeout");return LCCCR_CONTINUE;}en = LWS_ERRNO;lwsl_wsi_debug(wsi, "errno %d", en);if (FD_ISSET(wsi->desc.sockfd, &except_set)) {/* Failed to connect */lwsl_wsi_notice(wsi, "connect failed, select exception fd set");return LCCCR_FAILED;}if (!en || en == WSAEINVAL ||en == WSAEWOULDBLOCK ||en == WSAEALREADY) {lwsl_wsi_debug(wsi, "errno %d", en);return LCCCR_CONTINUE;}
#endiflwsl_wsi_notice(wsi, "connect check FAILED: %d",*real_errno || en);return LCCCR_FAILED;
}

select在这种使用场景下不会出现问题,可以正确返回连接成功,目前修改方案如上所示

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/887155.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

BTB08-ASEMI电气加热设备专用BTB08

BTB08-ASEMI电气加热设备专用BTB08编辑:ll BTB08-ASEMI电气加热设备专用BTB08 型号:BTB08 品牌:ASEMI 封装:TO-220F 正向电流:8A 反向电压:600V~800V 引脚数量:3 芯片个数:2 芯片尺寸: 漏电流:>10ua 恢复时间: 包装方式:管装 封装尺寸:如图 特性:双向可控硅 工…

如何在 NocoBase 中实现注册用户审核

旨在通过简洁的小需求示例与小技巧分享,帮助你快速熟悉并掌握 NocoBase 的核心功能与最佳实践,让你轻松上手、高效开发。本文档提供了两种实现用户注册审核的方案,针对不同的业务场景设计:方案一:适用于需要简单、快速地实现注册审核流程的场景。该方案利用系统默认的新用…

测试用例的方法-边界值

一、边界值定义 (1)上点:边界上的点 (2)离点:离上点最近的点 (3)内点:在域的范围内的点 在测试过程中根据实际情况定:闭区间:案例:5-13位qq号 上点:5,13 离点:4,14 内点:8(6,7,8,9,10,11,12) 测试:5 最小值,13 最大值,4小于最小值,14 大于最大值,8范…

测试用例方法-等价类

一、常用的设计方法: (1)黑盒测试方法: 等价类、边界值,判定表、因果图、正交表,场景法、状态迁移法; (2)经验测试方法: 错误推测法、异常分析法、随机测试; (3)白盒测试方法: 语句覆盖,判断覆盖,条件覆盖,判断、条件覆盖,路径覆盖(基本路径法、Z路径法) 二、详…

C#程序员转型——DeepSeek回答“请问C#程序员转到微软Dynamics 365 CRM系统和Power Platform开发上有什么好处?”——爱码士IT培训

C#程序员转型,将C#开发技能迁移到微软Dynamics 365 CRM和Power Platform开发领域,对职业发展和技术能力提升有显著优势。以下是Deepseek的具体的分析和建议: 1. 技术栈的天然延伸与增强 C#作为微软生态的核心语言,与Dynamics 365和Power Platform深度兼容,开发者可以快速上…

教培机构管理升级:如何通过项目管理实现高效运营?

教培机构项目管理 教培机构项目管理是指通过科学的管理方法和工具,对教育培训机构的各项活动进行规划、组织、实施和监控,以确保教学目标的高效达成和机构的可持续发展。以下是教培机构项目管理的核心内容及实施策略: 一、项目管理的核心内容 课程设计与开发 ○ 需求分析:通…

Ruoyi-Vue 3.8.7集成积木报表JmReport和积木大屏JimuBI

Ruoyi-Vue 3.8.7集成积木报表JmReport和积木大屏JimuBI 一、版本 RuoYi-Vue版本:v3.8.7 JMreport报表版本: v1.9.4 JimuBI大屏版本:V1.Ruoyi-Vue 3.8.7集成积木报表JmReport和积木大屏JimuBI 一、版本 RuoYi-Vue版本:v3.8.7 JMreport报表版本: v1.9.4 JimuBI大屏版本:V1.…

关于java中CAS会引发的ABA问题探究

在并发环境下,为了保证并发安全问题,通常我们会进行加锁操作,比如加上synchronized关键字。但是很多情况下,我们不需要这样的重量级锁,比如说多个线程对某个int类型变量i 进行++操作,但是不加锁吧,又怕影响结果,因为i++不是一个原子操作,会出现并发问题,我们来看个案例…

DHTMLX Gantt 甘特图导出全数据图-----自实现方式过程记录

针对上一篇DHTMLX Gantt甘特图导出全数据图,使用官方提供的方法虽然很方便,也免费,但每次生成图片的时候需要访问下:export.dhtmlx.com的相关授权接口,这对我们的项目如果是在内网(不能联网访问外网的情况)非常不友好,也就无法使用官方的方式了。 那么如果尽量能实现同…

jmter

1.安装 官网下载解压 2.改中文 bin/jmeter.properties 加入 language=zh_CN3.运行 bin/jmeter.bat 4.压测使用 线程组 -> HTTP请求 , HTTP请求头管理线程组 -> 汇总结果线程组属性线程数:配置几个就代表有几个虚拟用户 Ramp-Up 时间(秒):表示从第一个虚拟用户开始生成…

Dify + Ollama + DeepSeek 知识库部署

通过ollama安装deepseek-r1我没有GPU服务器,安装7b版本小模型试试。参考: https://github.com/ollama/ollama 安装ollama # ollama默认端口11434 curl -fsSL https://ollama.com/install.sh | sh ollama pull deepseek-r1:7b # 查看本地模型 ollama list ollama run deepseek-…

2025.2.20(IDEA的入门使用)

今天10点看了视频学习IDEA的基本使用方法。现在来记录一下。 https://www.bilibili.com/video/BV1gb42177hm?spm_id_from=333.788.videopod.episodes&p=7&vd_source=5573907594eed8092d5b30e989fa0415 IDEA创建Java的过程如图:一个大工程里面包含了各个模块,每个模块…