背景
使用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在这种使用场景下不会出现问题,可以正确返回连接成功,目前修改方案如上所示