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

背景

使用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)))
#else
				0))
#endif

		return 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来实现循环多次检查
#endif

	lwsl_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);
  // 第一次 connect
  int 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);

  // 第二次 connect
  result = ::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;
	}

#else

	fd_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;
	}
#endif

	lwsl_wsi_notice(wsi, "connect check FAILED: %d",
			*real_errno || en);

	return LCCCR_FAILED;
}

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

posted @ 2025-02-20 14:20  leno米雷  阅读(146)  评论(0)    收藏  举报